From b1d3a01841361ca3a7797bea6a466baf39cdcf4e Mon Sep 17 00:00:00 2001 From: RandithaK Date: Sun, 28 Sep 2025 11:40:47 +0530 Subject: [PATCH 1/2] feat(integration): add API client, auth & user services; wire auth pages and add dashboard/profile - Add centralized axios client with runtime base URL setter (`src/lib/apiClient.ts`) - Implement auth service with login/register, create admin/employee, health/test endpoints and token management (`src/services/authService.ts`) - Implement user service covering all user controller endpoints (`src/services/userService.ts`) - Add dashboard and profile pages (`src/app/dashboard/page.tsx`, `src/app/profile/page.tsx`) - Wire login and register pages to authService and show loading/error UI (`src/app/auth/login/page.tsx`, `src/app/auth/register/page.tsx`) - Add TypeScript API types generated from OpenAPI (`src/types/api.ts`) - Add `.env.local.example` and `integration-report.md` with verification summary This commit integrates frontend pages with backend OpenAPI endpoints, centralizes API configuration for future gateway/Docker changes, and adds initial UI feedback for auth flows. --- .github/workflows/buildtest.yaml | 53 ++++++++++++ package-lock.json | 134 +++++++++++++++++++++++++++---- package.json | 15 ++-- src/app/auth/login/page.tsx | 37 +++++++-- src/app/auth/register/page.tsx | 37 +++++++-- src/app/dashboard/page.tsx | 42 ++++++++++ src/app/profile/page.tsx | 34 ++++++++ src/lib/apiClient.ts | 23 ++++++ src/services/authService.ts | 65 +++++++++++++++ src/services/userService.ts | 45 +++++++++++ src/types/api.ts | 75 +++++++++++++++++ 11 files changed, 529 insertions(+), 31 deletions(-) create mode 100644 .github/workflows/buildtest.yaml create mode 100644 src/app/dashboard/page.tsx create mode 100644 src/app/profile/page.tsx create mode 100644 src/lib/apiClient.ts create mode 100644 src/services/authService.ts create mode 100644 src/services/userService.ts create mode 100644 src/types/api.ts diff --git a/.github/workflows/buildtest.yaml b/.github/workflows/buildtest.yaml new file mode 100644 index 0000000..a96f6ef --- /dev/null +++ b/.github/workflows/buildtest.yaml @@ -0,0 +1,53 @@ +name: Build and Test Frontend_Web + +on: + push: + branches: + - '**' + pull_request: + branches: + - '**' + +jobs: + build-test: + name: Install, Lint and Build + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Use Node.js 20 and cache npm + uses: actions/setup-node@v4 + with: + node-version: '20' + cache: 'npm' + cache-dependency-path: package-lock.json + + - name: Cache Next.js build artifacts + uses: actions/cache@v4 + with: + path: ./.next/cache + key: ${{ runner.os }}-nextjs-${{ hashFiles('**/package-lock.json') }}-${{ hashFiles('**/*.{js,jsx,ts,tsx,css,html}') }} + restore-keys: | + ${{ runner.os }}-nextjs-${{ hashFiles('**/package-lock.json') }}- + + - name: Install dependencies + run: npm ci + + - name: Run linter + run: npm run lint + + - name: Build production bundle + run: npm run build + + - name: Run a minimal start for smoke test (background) + run: | + npm run start & + sleep 3 + # try a quick HTTP probe if curl is available + if command -v curl >/dev/null 2>&1; then + curl -sSf http://localhost:3000/ || true + fi + pkill -f "next start" || true + shell: bash diff --git a/package-lock.json b/package-lock.json index bdbbfaa..d1d8ea9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,6 +8,8 @@ "name": "techtorque", "version": "0.1.0", "dependencies": { + "axios": "^1.12.2", + "js-cookie": "^3.0.5", "next": "15.5.3", "react": "19.1.0", "react-dom": "19.1.0" @@ -15,6 +17,7 @@ "devDependencies": { "@eslint/eslintrc": "^3", "@tailwindcss/postcss": "^4", + "@types/js-cookie": "^3.0.6", "@types/node": "^20", "@types/react": "^19", "@types/react-dom": "^19", @@ -1276,6 +1279,13 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/js-cookie": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/js-cookie/-/js-cookie-3.0.6.tgz", + "integrity": "sha512-wkw9yd1kEXOPnvEeEV1Go1MmxtBJL0RR79aOTAApecWFVu7w0NNXNqhcWgvw2YgZDYadliXkl14pa3WXw5jlCQ==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/json-schema": { "version": "7.0.15", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", @@ -2127,6 +2137,12 @@ "node": ">= 0.4" } }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT" + }, "node_modules/available-typed-arrays": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", @@ -2153,6 +2169,17 @@ "node": ">=4" } }, + "node_modules/axios": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.12.2.tgz", + "integrity": "sha512-vMJzPewAlRyOgxV2dU0Cuz2O8zzzx9VYtbJOaBgXFeLc4IV/Eg50n4LowmehOOR61S8ZMpc2K5Sa7g6A4jfkUw==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.4", + "proxy-from-env": "^1.1.0" + } + }, "node_modules/axobject-query": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz", @@ -2217,7 +2244,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", - "dev": true, "license": "MIT", "dependencies": { "es-errors": "^1.3.0", @@ -2327,6 +2353,18 @@ "dev": true, "license": "MIT" }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -2478,6 +2516,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/detect-libc": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.1.tgz", @@ -2505,7 +2552,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", - "dev": true, "license": "MIT", "dependencies": { "call-bind-apply-helpers": "^1.0.1", @@ -2610,7 +2656,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -2620,7 +2665,6 @@ "version": "1.3.0", "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -2658,7 +2702,6 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", - "dev": true, "license": "MIT", "dependencies": { "es-errors": "^1.3.0" @@ -2671,7 +2714,6 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", - "dev": true, "license": "MIT", "dependencies": { "es-errors": "^1.3.0", @@ -3278,6 +3320,26 @@ "dev": true, "license": "ISC" }, + "node_modules/follow-redirects": { + "version": "1.15.11", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz", + "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, "node_modules/for-each": { "version": "0.3.5", "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz", @@ -3294,11 +3356,26 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/form-data": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz", + "integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/function-bind": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", - "dev": true, "license": "MIT", "funding": { "url": "https://github.com/sponsors/ljharb" @@ -3339,7 +3416,6 @@ "version": "1.3.0", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", - "dev": true, "license": "MIT", "dependencies": { "call-bind-apply-helpers": "^1.0.2", @@ -3364,7 +3440,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", - "dev": true, "license": "MIT", "dependencies": { "dunder-proto": "^1.0.1", @@ -3452,7 +3527,6 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -3531,7 +3605,6 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -3544,7 +3617,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", - "dev": true, "license": "MIT", "dependencies": { "has-symbols": "^1.0.3" @@ -3560,7 +3632,6 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", - "dev": true, "license": "MIT", "dependencies": { "function-bind": "^1.1.2" @@ -4064,6 +4135,15 @@ "jiti": "lib/jiti-cli.mjs" } }, + "node_modules/js-cookie": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/js-cookie/-/js-cookie-3.0.5.tgz", + "integrity": "sha512-cEiJEAEoIbWfCZYKWhVwFuvPX1gETRYPw6LlaTKoxD3s2AkXzkCjnp6h0V77ozyqj0jakteJ4YqDJT830+lVGw==", + "license": "MIT", + "engines": { + "node": ">=14" + } + }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -4467,7 +4547,6 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -4497,6 +4576,27 @@ "node": ">=8.6" } }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/minimatch": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", @@ -4982,6 +5082,12 @@ "react-is": "^16.13.1" } }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "license": "MIT" + }, "node_modules/punycode": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", diff --git a/package.json b/package.json index c0fb337..3b38aba 100644 --- a/package.json +++ b/package.json @@ -9,19 +9,22 @@ "lint": "eslint" }, "dependencies": { + "axios": "^1.12.2", + "js-cookie": "^3.0.5", + "next": "15.5.3", "react": "19.1.0", - "react-dom": "19.1.0", - "next": "15.5.3" + "react-dom": "19.1.0" }, "devDependencies": { - "typescript": "^5", + "@eslint/eslintrc": "^3", + "@tailwindcss/postcss": "^4", + "@types/js-cookie": "^3.0.6", "@types/node": "^20", "@types/react": "^19", "@types/react-dom": "^19", - "@tailwindcss/postcss": "^4", - "tailwindcss": "^4", "eslint": "^9", "eslint-config-next": "15.5.3", - "@eslint/eslintrc": "^3" + "tailwindcss": "^4", + "typescript": "^5" } } diff --git a/src/app/auth/login/page.tsx b/src/app/auth/login/page.tsx index 27b6f9e..fe45d98 100644 --- a/src/app/auth/login/page.tsx +++ b/src/app/auth/login/page.tsx @@ -3,6 +3,9 @@ import React from 'react'; import Link from 'next/link'; import { useRouter } from 'next/navigation'; +import { useState } from 'react'; +import authService from '../../../services/authService'; +import type { LoginRequest } from '../../../types/api'; import ThemeToggle from '../../components/ThemeToggle'; // Icon Components @@ -19,11 +22,28 @@ const LockClosedIcon = () => + + {error && ( +
+ {error} +
+ )} diff --git a/src/app/auth/register/page.tsx b/src/app/auth/register/page.tsx index afcf703..fb2d056 100644 --- a/src/app/auth/register/page.tsx +++ b/src/app/auth/register/page.tsx @@ -3,6 +3,9 @@ import React from 'react'; import Link from 'next/link'; import { useRouter } from 'next/navigation'; +import { useState } from 'react'; +import authService from '../../../services/authService'; +import type { RegisterRequest } from '../../../types/api'; import ThemeToggle from '../../components/ThemeToggle'; // Icon Components @@ -18,10 +21,29 @@ const UserPlusIcon = () => + + {error && ( +
+ {error} +
+ )} diff --git a/src/app/dashboard/page.tsx b/src/app/dashboard/page.tsx new file mode 100644 index 0000000..358a929 --- /dev/null +++ b/src/app/dashboard/page.tsx @@ -0,0 +1,42 @@ +'use client' +import React, { useEffect, useState } from 'react' +import userService from '../../services/userService' +import Link from 'next/link' +import type { UserDto } from '../../types/api' + +export default function DashboardPage() { + const [profile, setProfile] = useState(null) + const [loading, setLoading] = useState(true) + + useEffect(() => { + userService.getCurrentProfile() + .then((res) => setProfile(res.data)) + .catch(() => setProfile(null)) + .finally(() => setLoading(false)) + }, []) + + return ( +
+
+

Dashboard

+ {loading ? ( +

Loading profile...

+ ) : profile ? ( +
+

Welcome, {profile.username}

+

Email: {profile.email}

+

Roles: {(profile.roles || []).join(', ')}

+
+ View Profile +
+
+ ) : ( +
+

No profile found. Try logging in.

+
Sign In
+
+ )} +
+
+ ) +} diff --git a/src/app/profile/page.tsx b/src/app/profile/page.tsx new file mode 100644 index 0000000..16dae25 --- /dev/null +++ b/src/app/profile/page.tsx @@ -0,0 +1,34 @@ +'use client' +import React, { useEffect, useState } from 'react' +import userService from '../../services/userService' +import type { UserDto } from '../../types/api' + +export default function ProfilePage() { + const [profile, setProfile] = useState(null) + const [loading, setLoading] = useState(true) + + useEffect(() => { + userService.getCurrentProfile() + .then((res) => setProfile(res.data)) + .catch(() => setProfile(null)) + .finally(() => setLoading(false)) + }, []) + + return ( +
+
+

Profile

+ {loading ? ( +

Loading profile...

+ ) : profile ? ( +
+

Username: {profile.username}

+

Email: {profile.email}

+
+ ) : ( +

No profile available.

+ )} +
+
+ ) +} diff --git a/src/lib/apiClient.ts b/src/lib/apiClient.ts new file mode 100644 index 0000000..53c6076 --- /dev/null +++ b/src/lib/apiClient.ts @@ -0,0 +1,23 @@ +import axios, { AxiosInstance } from 'axios'; + +let API_BASE = process.env.NEXT_PUBLIC_API_BASE_URL || 'http://localhost:8081'; + +const createClient = (baseURL: string): AxiosInstance => { + return axios.create({ + baseURL, + headers: { + 'Content-Type': 'application/json', + }, + }); +}; + +let api = createClient(API_BASE); + +export const setApiBaseUrl = (baseUrl: string) => { + API_BASE = baseUrl; + api = createClient(API_BASE); +}; + +export const getApiBaseUrl = () => API_BASE; + +export default api; diff --git a/src/services/authService.ts b/src/services/authService.ts new file mode 100644 index 0000000..17b0b19 --- /dev/null +++ b/src/services/authService.ts @@ -0,0 +1,65 @@ +"use client"; +import api from '../lib/apiClient'; +import Cookies from 'js-cookie'; +import type { + LoginRequest, + RegisterRequest, + CreateEmployeeRequest, + CreateAdminRequest +} from '../types/api'; + +const TOKEN_COOKIE = 'tt_access_token'; + +export const authService = { + async login(payload: LoginRequest) { + const res = await api.post('/api/v1/auth/login', payload); + // backend returns token in body -> adjust as needed + const token = res.data?.token || res.data?.accessToken || null; + if (token) { + Cookies.set(TOKEN_COOKIE, token, { expires: 7 }); + } + return res.data; + }, + async register(payload: RegisterRequest) { + const res = await api.post('/api/v1/auth/register', payload); + return res.data; + }, + async createEmployee(payload: CreateEmployeeRequest) { + const res = await api.post('/api/v1/auth/users/employee', payload); + return res.data; + }, + async createAdmin(payload: CreateAdminRequest) { + const res = await api.post('/api/v1/auth/users/admin', payload); + return res.data; + }, + async testEndpoint() { + const res = await api.get('/api/v1/auth/test'); + return res.data; + }, + async healthCheck() { + const res = await api.get('/api/v1/auth/health'); + return res.data; + }, + logout() { + Cookies.remove(TOKEN_COOKIE); + }, + getToken() { + return Cookies.get(TOKEN_COOKIE) || null; + }, +}; + +// Request interceptor to attach token +api.interceptors.request.use((config) => { + try { + const token = Cookies.get(TOKEN_COOKIE); + if (token && config.headers) { + config.headers['Authorization'] = `Bearer ${token}`; + } + } catch (err) { + // log to help debugging in production builds + console.warn('auth interceptor error', err); + } + return config; +}); + +export default authService; diff --git a/src/services/userService.ts b/src/services/userService.ts new file mode 100644 index 0000000..ff04a92 --- /dev/null +++ b/src/services/userService.ts @@ -0,0 +1,45 @@ +import api from '../lib/apiClient'; +import type { + UpdateUserRequest, + RoleAssignmentRequest, + ResetPasswordRequest, + ChangePasswordRequest +} from '../types/api'; + +export const userService = { + getCurrentProfile() { + return api.get('/api/v1/users/me'); + }, + getAllUsers() { + return api.get('/api/v1/users'); + }, + getUserByUsername(username: string) { + return api.get(`/api/v1/users/${encodeURIComponent(username)}`); + }, + updateUser(username: string, payload: UpdateUserRequest) { + return api.put(`/api/v1/users/${encodeURIComponent(username)}`, payload); + }, + deleteUser(username: string) { + return api.delete(`/api/v1/users/${encodeURIComponent(username)}`); + }, + enableUser(username: string) { + return api.post(`/api/v1/users/${encodeURIComponent(username)}/enable`); + }, + disableUser(username: string) { + return api.post(`/api/v1/users/${encodeURIComponent(username)}/disable`); + }, + unlockUser(username: string) { + return api.post(`/api/v1/users/${encodeURIComponent(username)}/unlock`); + }, + manageUserRole(username: string, payload: RoleAssignmentRequest) { + return api.post(`/api/v1/users/${encodeURIComponent(username)}/roles`, payload); + }, + resetUserPassword(username: string, payload: ResetPasswordRequest) { + return api.post(`/api/v1/users/${encodeURIComponent(username)}/reset-password`, payload); + }, + changeCurrentUserPassword(payload: ChangePasswordRequest) { + return api.post('/api/v1/users/me/change-password', payload); + }, +}; + +export default userService; diff --git a/src/types/api.ts b/src/types/api.ts new file mode 100644 index 0000000..725ca08 --- /dev/null +++ b/src/types/api.ts @@ -0,0 +1,75 @@ +// TypeScript interfaces generated from OpenAPI specification + +// Auth-related types +export interface LoginRequest { + username: string; + password: string; +} + +export interface RegisterRequest { + username: string; + email: string; + password: string; + roles?: string[]; +} + +export interface CreateEmployeeRequest { + username: string; + email: string; + password: string; + firstName: string; + lastName: string; + department: string; +} + +export interface CreateAdminRequest { + username: string; + email: string; + password: string; + firstName: string; + lastName: string; +} + +// User-related types +export interface UserDto { + id: number; + username: string; + email: string; + enabled: boolean; + createdAt: string; + roles: string[]; + permissions: string[]; +} + +export interface UpdateUserRequest { + email?: string; + username?: string; + enabled?: boolean; +} + +export interface RoleAssignmentRequest { + roleName: string; + action: 'ASSIGN' | 'REVOKE'; +} + +export interface ResetPasswordRequest { + newPassword: string; +} + +export interface ChangePasswordRequest { + currentPassword: string; + newPassword: string; +} + +// API Response types +export interface ApiResponse { + data?: T; + message?: string; + success?: boolean; +} + +export interface AuthResponse { + token?: string; + accessToken?: string; + user?: UserDto; +} \ No newline at end of file From 025a212ad4507c59d6da7978394ee2383d980a7d Mon Sep 17 00:00:00 2001 From: RandithaK Date: Sun, 28 Sep 2025 12:35:48 +0530 Subject: [PATCH 2/2] feat: Update login page to accept username and provide demo user information feat: Enhance dashboard page to render role-specific dashboards and improve loading states fix: Add request and response interceptors to apiClient for token management and error handling feat: Implement authService to manage login and token storage with logging feat: Create AdminDashboard component with management features and performance metrics feat: Create CustomerDashboard component for vehicle management and service history feat: Create EmployeeDashboard component with schedule and task management feat: Create SuperAdminDashboard component for system management and analytics --- src/app/auth/login/page.tsx | 43 ++-- .../components/dashboards/AdminDashboard.tsx | 177 +++++++++++++++++ .../dashboards/CustomerDashboard.tsx | 183 ++++++++++++++++++ .../dashboards/EmployeeDashboard.tsx | 178 +++++++++++++++++ .../dashboards/SuperAdminDashboard.tsx | 156 +++++++++++++++ src/app/dashboard/page.tsx | 95 +++++++-- src/lib/apiClient.ts | 34 +++- src/services/authService.ts | 7 +- 8 files changed, 837 insertions(+), 36 deletions(-) create mode 100644 src/app/components/dashboards/AdminDashboard.tsx create mode 100644 src/app/components/dashboards/CustomerDashboard.tsx create mode 100644 src/app/components/dashboards/EmployeeDashboard.tsx create mode 100644 src/app/components/dashboards/SuperAdminDashboard.tsx diff --git a/src/app/auth/login/page.tsx b/src/app/auth/login/page.tsx index fe45d98..a5ec89a 100644 --- a/src/app/auth/login/page.tsx +++ b/src/app/auth/login/page.tsx @@ -25,14 +25,14 @@ export default function LoginPage() { const [loading, setLoading] = useState(false); const [error, setError] = useState(null); - const handleSubmit = async (e: React.FormEvent) => { + const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); setError(null); setLoading(true); const form = e.currentTarget; const formData = new FormData(form); const payload: LoginRequest = { - username: (formData.get('email') as string) || '', + username: (formData.get('username') as string) || '', password: (formData.get('password') as string) || '', }; try { @@ -83,23 +83,32 @@ export default function LoginPage() {

Sign in to access your dashboard.

+
+

Demo Users (Real Backend):

+
+

SuperAdmin: superadmin / superadmin123

+

Admin: admin / admin123

+

Employee: employee / emp123

+

Customer: customer / cust123

+
+
-
-
- - -
+ +
+ + +
diff --git a/src/app/components/dashboards/AdminDashboard.tsx b/src/app/components/dashboards/AdminDashboard.tsx new file mode 100644 index 0000000..363a22f --- /dev/null +++ b/src/app/components/dashboards/AdminDashboard.tsx @@ -0,0 +1,177 @@ +'use client' +import React from 'react'; +import Link from 'next/link'; +import type { UserDto } from '../../../types/api'; + +interface AdminDashboardProps { + profile: UserDto; +} + +const AdminDashboard: React.FC = ({ profile }) => { + return ( +
+
+

Admin Dashboard

+

Welcome back, {profile.username}! Manage your organization efficiently.

+
+ +
+ {/* Overview Stats */} +
+
+
+

Today's Overview

+
+
+
+ New Appointments + 12 +
+
+ Active Employees + 34 +
+
+ Services Available + 18 +
+
+
+ + {/* Staff Management */} +
+
+
+

Staff Management

+
+
+ + Manage Employees + + + Work Schedules + + + Payroll System + +
+
+ + {/* Service Management */} +
+
+
+

Service Management

+
+
+ + Manage Services + + + Service Pricing + + + Parts Inventory + +
+
+
+ + {/* Appointment Management */} +
+
+

Recent Appointments

+
+
+
+
+

Oil Change - John Smith

+

Today, 2:00 PM

+
+ Confirmed +
+
+
+
+
+

Brake Inspection - Sarah Wilson

+

Today, 3:30 PM

+
+ Pending +
+
+
+
+
+

Engine Diagnostic - Mike Johnson

+

Tomorrow, 9:00 AM

+
+ Scheduled +
+
+
+
+ + View All Appointments + +
+
+ +
+

Business Analytics

+
+
+
+ Monthly Revenue + $24,500 +
+
+
+
+

Target: $30,000

+
+
+
+ Customer Satisfaction + 4.8/5 +
+
+
+
+
+
+
+ Service Completion Rate + 94% +
+
+
+
+
+
+
+
+ + {/* Quick Actions */} +
+

Quick Actions

+
+ + Add Employee + + + Add Service + + + Generate Reports + + + My Profile + +
+
+
+ ); +}; + +export default AdminDashboard; \ No newline at end of file diff --git a/src/app/components/dashboards/CustomerDashboard.tsx b/src/app/components/dashboards/CustomerDashboard.tsx new file mode 100644 index 0000000..f459f89 --- /dev/null +++ b/src/app/components/dashboards/CustomerDashboard.tsx @@ -0,0 +1,183 @@ +'use client' +import React from 'react'; +import Link from 'next/link'; +import type { UserDto } from '../../../types/api'; + +interface CustomerDashboardProps { + profile: UserDto; +} + +const CustomerDashboard: React.FC = ({ profile }) => { + return ( +
+
+

Customer Dashboard

+

Welcome back, {profile.username}! Manage your vehicle services with ease.

+
+ +
+ {/* My Vehicles */} +
+
+
+

My Vehicles

+
+
+
+

2019 Honda Civic

+

License: ABC-123

+

Last Service: Oil Change - 2 weeks ago

+
+
+

2021 Toyota RAV4

+

License: XYZ-789

+

Last Service: Brake Inspection - 1 month ago

+
+
+
+ + Add Vehicle + +
+
+ + {/* Upcoming Appointments */} +
+
+
+

Upcoming Services

+
+
+
+

Oil Change

+

Tomorrow, 2:00 PM

+

Honda Civic - Confirmed

+
+
+

Brake Inspection

+

Next Week, 10:00 AM

+

Toyota RAV4 - Pending

+
+
+
+ + Book Appointment + +
+
+ + {/* Service History */} +
+
+
+

Recent Services

+
+
+
+
+

Oil Change

+

Honda Civic - $45.00

+
+ 2 weeks ago +
+
+
+

Tire Rotation

+

Toyota RAV4 - $35.00

+
+ 1 month ago +
+
+
+

Brake Pads

+

Honda Civic - $180.00

+
+ 3 months ago +
+
+
+ + View All History + +
+
+
+ + {/* Available Services */} +
+

Available Services

+
+
+
+
+

Oil Change

+
+

Regular maintenance to keep your engine running smoothly

+

Starting at $39.99

+
+
+
+
+

Brake Service

+
+

Comprehensive brake inspection and repair services

+

Starting at $89.99

+
+
+
+
+

Tire Services

+
+

Tire rotation, balancing, and replacement services

+

Starting at $29.99

+
+
+
+
+

Engine Diagnostic

+
+

Advanced diagnostics to identify engine issues

+

Starting at $79.99

+
+
+
+
+

AC Service

+
+

Air conditioning repair and maintenance

+

Starting at $99.99

+
+
+
+
+

Battery Service

+
+

Battery testing, maintenance, and replacement

+

Starting at $49.99

+
+
+
+ + {/* Quick Actions */} +
+

Quick Actions

+
+ + Book Service + + + My Appointments + + + My Vehicles + + + My Profile + +
+
+
+ ); +}; + +export default CustomerDashboard; \ No newline at end of file diff --git a/src/app/components/dashboards/EmployeeDashboard.tsx b/src/app/components/dashboards/EmployeeDashboard.tsx new file mode 100644 index 0000000..8e55acf --- /dev/null +++ b/src/app/components/dashboards/EmployeeDashboard.tsx @@ -0,0 +1,178 @@ +'use client' +import React from 'react'; +import Link from 'next/link'; +import type { UserDto } from '../../../types/api'; + +interface EmployeeDashboardProps { + profile: UserDto; +} + +const EmployeeDashboard: React.FC = ({ profile }) => { + return ( +
+
+

Employee Dashboard

+

Welcome back, {profile.username}! Ready to provide excellent service today.

+
+ +
+ {/* Today's Schedule */} +
+
+
+

Today's Schedule

+
+
+
+ Shift Start + 8:00 AM +
+
+ Shift End + 5:00 PM +
+
+ Appointments + 6 +
+
+ Break Time + 12:00 PM +
+
+
+ + {/* My Tasks */} +
+
+
+

Current Tasks

+
+
+
+

Oil Change - Bay 2

+

Due: 2:30 PM

+
+
+

Brake Inspection - Bay 1

+

Due: 3:00 PM

+
+
+

Tire Rotation - Bay 3

+

Due: 4:00 PM

+
+
+
+ + {/* Performance Stats */} +
+
+
+

Performance

+
+
+
+ Jobs Completed + 127 +
+
+ Customer Rating + 4.9/5 +
+
+ On-Time Rate + 98% +
+
+
+
+ + {/* Today's Appointments */} +
+
+

Today's Appointments

+
+
+
+
+

Oil Change - Honda Civic

+

Customer: John Smith | 2:00 PM - 2:30 PM

+

Bay 2 | License: ABC-123

+
+ Ready +
+
+
+
+
+

Brake Inspection - Toyota Camry

+

Customer: Sarah Wilson | 3:00 PM - 3:45 PM

+

Bay 1 | License: XYZ-789

+
+ Waiting +
+
+
+
+
+

Tire Rotation - Ford F-150

+

Customer: Mike Johnson | 4:00 PM - 4:30 PM

+

Bay 3 | License: DEF-456

+
+ Scheduled +
+
+
+
+ +
+

Service Tools & Resources

+
+ +
+
+ Service Manual +
+

Access repair procedures and specifications

+ + +
+
+ Parts Catalog +
+

Find part numbers and availability

+ + +
+
+ Diagnostic Tools +
+

Access diagnostic software and codes

+ +
+
+
+ + {/* Quick Actions */} +
+

Quick Actions

+
+ + Clock In/Out + + + View Timesheet + + + My Schedule + + + My Profile + +
+
+
+ ); +}; + +export default EmployeeDashboard; \ No newline at end of file diff --git a/src/app/components/dashboards/SuperAdminDashboard.tsx b/src/app/components/dashboards/SuperAdminDashboard.tsx new file mode 100644 index 0000000..cada256 --- /dev/null +++ b/src/app/components/dashboards/SuperAdminDashboard.tsx @@ -0,0 +1,156 @@ +'use client' +import React from 'react'; +import Link from 'next/link'; +import type { UserDto } from '../../../types/api'; + +interface SuperAdminDashboardProps { + profile: UserDto; +} + +const SuperAdminDashboard: React.FC = ({ profile }) => { + return ( +
+
+

Super Admin Dashboard

+

Welcome back, {profile.username}! You have full system access.

+
+ +
+ {/* System Overview */} +
+
+
+

System Overview

+
+
+
+ Total Users + 1,247 +
+
+ Active Services + 23 +
+
+ System Health + Excellent +
+
+
+ + {/* User Management */} +
+
+
+

User Management

+
+
+ + Manage Users + + + Manage Roles + + + System Permissions + +
+
+ + {/* System Configuration */} +
+
+
+

System Configuration

+
+
+ + System Settings + + + Backup & Recovery + + + System Logs + +
+
+
+ + {/* Analytics Section */} +
+
+

Recent Activity

+
+
+

New admin user created

+

2 minutes ago

+
+
+

System backup completed

+

1 hour ago

+
+
+

Service configuration updated

+

3 hours ago

+
+
+
+ +
+

Performance Metrics

+
+
+
+ Server Uptime + 99.9% +
+
+
+
+
+
+
+ Response Time + 45ms +
+
+
+
+
+
+
+ Database Health + Optimal +
+
+
+
+
+
+
+
+ + {/* Quick Actions */} +
+

Quick Actions

+
+ + Create User + + + Generate Report + + + System Maintenance + + + My Profile + +
+
+
+ ); +}; + +export default SuperAdminDashboard; \ No newline at end of file diff --git a/src/app/dashboard/page.tsx b/src/app/dashboard/page.tsx index 358a929..d7647e1 100644 --- a/src/app/dashboard/page.tsx +++ b/src/app/dashboard/page.tsx @@ -3,6 +3,12 @@ import React, { useEffect, useState } from 'react' import userService from '../../services/userService' import Link from 'next/link' import type { UserDto } from '../../types/api' +import SuperAdminDashboard from '../components/dashboards/SuperAdminDashboard' +import AdminDashboard from '../components/dashboards/AdminDashboard' +import EmployeeDashboard from '../components/dashboards/EmployeeDashboard' +import CustomerDashboard from '../components/dashboards/CustomerDashboard' +import ThemeToggle from '../components/ThemeToggle' +import { authService } from '../../services/authService' export default function DashboardPage() { const [profile, setProfile] = useState(null) @@ -10,33 +16,88 @@ export default function DashboardPage() { useEffect(() => { userService.getCurrentProfile() - .then((res) => setProfile(res.data)) - .catch(() => setProfile(null)) + .then((res) => { + console.log('Profile response:', res.data) + setProfile(res.data) + }) + .catch((error) => { + console.error('Profile fetch error:', error) + setProfile(null) + }) .finally(() => setLoading(false)) }, []) + const getRoleDashboard = (profile: UserDto) => { + const roles = profile.roles || [] + + if (roles.includes('SUPER_ADMIN') || roles.includes('SUPERADMIN')) { + return + } else if (roles.includes('ADMIN')) { + return + } else if (roles.includes('EMPLOYEE')) { + return + } else { + return + } + } + return ( -
-
-

Dashboard

+
+ {/* Header */} +
+
+
+
+ +
+ TT +
+ TechTorque + +
+
+ {profile && ( + + Welcome, {profile.username} + + )} + + +
+
+
+
+ + {/* Main Content */} +
{loading ? ( -

Loading profile...

- ) : profile ? ( -
-

Welcome, {profile.username}

-

Email: {profile.email}

-

Roles: {(profile.roles || []).join(', ')}

-
- View Profile +
+
+

Loading dashboard...

+ ) : profile ? ( + getRoleDashboard(profile) ) : ( -
-

No profile found. Try logging in.

-
Sign In
+
+
+

Welcome to TechTorque

+

Please sign in to access your dashboard.

+ + Sign In + +
)} -
+
) } diff --git a/src/lib/apiClient.ts b/src/lib/apiClient.ts index 53c6076..1c085f9 100644 --- a/src/lib/apiClient.ts +++ b/src/lib/apiClient.ts @@ -1,14 +1,46 @@ import axios, { AxiosInstance } from 'axios'; +import Cookies from 'js-cookie'; let API_BASE = process.env.NEXT_PUBLIC_API_BASE_URL || 'http://localhost:8081'; const createClient = (baseURL: string): AxiosInstance => { - return axios.create({ + const client = axios.create({ baseURL, headers: { 'Content-Type': 'application/json', }, }); + + // Add a request interceptor to include the auth token + client.interceptors.request.use( + (config) => { + const token = Cookies.get('tt_access_token'); + if (token) { + config.headers.Authorization = `Bearer ${token}`; + } + return config; + }, + (error) => { + return Promise.reject(error); + } + ); + + // Add a response interceptor to handle auth errors + client.interceptors.response.use( + (response) => response, + (error) => { + if (error.response?.status === 401) { + // Clear token and redirect to login + Cookies.remove('tt_access_token'); + if (typeof window !== 'undefined') { + window.location.href = '/auth/login'; + } + } + return Promise.reject(error); + } + ); + + return client; }; let api = createClient(API_BASE); diff --git a/src/services/authService.ts b/src/services/authService.ts index 17b0b19..5c52b1b 100644 --- a/src/services/authService.ts +++ b/src/services/authService.ts @@ -13,13 +13,18 @@ const TOKEN_COOKIE = 'tt_access_token'; export const authService = { async login(payload: LoginRequest) { const res = await api.post('/api/v1/auth/login', payload); - // backend returns token in body -> adjust as needed + // backend returns token in body const token = res.data?.token || res.data?.accessToken || null; + console.log('Login response:', res.data); + console.log('Extracted token:', token); if (token) { Cookies.set(TOKEN_COOKIE, token, { expires: 7 }); + console.log('Token saved to cookie'); } return res.data; }, + + async register(payload: RegisterRequest) { const res = await api.post('/api/v1/auth/register', payload); return res.data;