From 5c6ab92056c6d0904b20ff8f2f8343a4bc628afe Mon Sep 17 00:00:00 2001 From: pradeepkumar Date: Mon, 22 Dec 2025 11:53:53 +0530 Subject: [PATCH] feat: Implement `AuthService` for user registration with detailed field error handling and updated user roles. --- package-lock.json | 116 +++++++++++++++++++++--- package.json | 1 + src/app/signup/page.tsx | 171 +++++++++++++++++++++++------------ src/services/api.ts | 71 +++++++++++++++ src/services/auth.service.ts | 146 ++++++++++++++++++++++++++++++ src/services/index.ts | 16 ++++ 6 files changed, 447 insertions(+), 74 deletions(-) create mode 100644 src/services/api.ts create mode 100644 src/services/auth.service.ts create mode 100644 src/services/index.ts diff --git a/package-lock.json b/package-lock.json index e5c7320..6565a06 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,6 +8,7 @@ "name": "web", "version": "0.1.0", "dependencies": { + "axios": "^1.13.2", "next": "16.1.0", "next-auth": "^5.0.0-beta.30", "react": "19.2.3", @@ -2402,6 +2403,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", @@ -2428,6 +2435,17 @@ "node": ">=4" } }, + "node_modules/axios": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.2.tgz", + "integrity": "sha512-VPk9ebNqPcy5lRGuSlKx752IlDatOjT9paPlm8A7yOuW2Fbvp4X3JznJtT4f0GzGLLiWE9W8onz51SqLYwzGaA==", + "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", @@ -2535,7 +2553,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", @@ -2635,6 +2652,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", @@ -2793,6 +2822,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.2", "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", @@ -2820,7 +2858,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", @@ -2932,7 +2969,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" @@ -2942,7 +2978,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" @@ -2980,7 +3015,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" @@ -2993,7 +3027,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", @@ -3626,6 +3659,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", @@ -3642,11 +3695,26 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/form-data": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", + "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", + "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" @@ -3707,7 +3775,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", @@ -3732,7 +3799,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", @@ -3820,7 +3886,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" @@ -3892,7 +3957,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" @@ -3905,7 +3969,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" @@ -3921,7 +3984,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" @@ -4913,7 +4975,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" @@ -4943,6 +5004,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", @@ -5468,6 +5550,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 aff7627..6a42261 100644 --- a/package.json +++ b/package.json @@ -9,6 +9,7 @@ "lint": "eslint" }, "dependencies": { + "axios": "^1.13.2", "next": "16.1.0", "next-auth": "^5.0.0-beta.30", "react": "19.2.3", diff --git a/src/app/signup/page.tsx b/src/app/signup/page.tsx index 6afaae9..69b2760 100644 --- a/src/app/signup/page.tsx +++ b/src/app/signup/page.tsx @@ -4,42 +4,50 @@ import { useState } from 'react'; import { useRouter } from 'next/navigation'; import { signIn } from 'next-auth/react'; import Link from 'next/link'; +import { authService, AuthService, RegisterRequest } from '@/services'; -type UserType = 'user' | 'admin'; +type UserType = 'USER' | 'AGENT'; + +interface ValidationError { + field: string; + errors: string[]; +} export default function SignUpPage() { const router = useRouter(); - const [userType, setUserType] = useState('user'); - const [name, setName] = useState(''); + const [userType, setUserType] = useState('USER'); + const [firstName, setFirstName] = useState(''); + const [lastName, setLastName] = useState(''); const [email, setEmail] = useState(''); const [password, setPassword] = useState(''); const [showPassword, setShowPassword] = useState(false); const [error, setError] = useState(''); + const [fieldErrors, setFieldErrors] = useState([]); const [loading, setLoading] = useState(false); + const getFieldError = (fieldName: string): string | null => { + const fieldError = fieldErrors.find((e) => e.field === fieldName); + return fieldError ? fieldError.errors[0] : null; + }; + const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); setLoading(true); setError(''); + setFieldErrors([]); try { - const response = await fetch('/api/auth/register', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ - name, - email, - password, - role: userType.toUpperCase(), - }), - }); + const registerData: RegisterRequest = { + firstName, + lastName, + email, + password, + role: userType, + }; - const data = await response.json(); - - if (!response.ok) { - throw new Error(data.message || 'Registration failed'); - } + await authService.register(registerData); + // Auto login after successful registration const result = await signIn('credentials', { email, password, @@ -53,7 +61,14 @@ export default function SignUpPage() { router.refresh(); } } catch (err) { - setError(err instanceof Error ? err.message : 'Registration failed'); + // Get field errors + const errors = AuthService.getFieldErrors(err); + if (errors.length > 0) { + setFieldErrors(errors); + setError('Please fix the errors below'); + } else { + setError(AuthService.getErrorMessage(err)); + } } finally { setLoading(false); } @@ -88,9 +103,9 @@ export default function SignUpPage() {
@@ -119,16 +134,37 @@ export default function SignUpPage() { )} - {/* Name Input */} -
- setName(e.target.value)} - required - className="w-full px-6 py-6 bg-[#f0f5fc] rounded-[20px] text-[#00293d] text-sm font-light placeholder:text-[#00293d] focus:outline-none focus:ring-2 focus:ring-[#00293d]/20 transition-all" - /> + {/* First Name & Last Name */} +
+
+ setFirstName(e.target.value)} + required + className={`w-full px-6 py-6 bg-[#f0f5fc] rounded-[20px] text-[#00293d] text-sm font-light placeholder:text-[#00293d] focus:outline-none focus:ring-2 focus:ring-[#00293d]/20 transition-all ${ + getFieldError('firstName') ? 'ring-2 ring-red-400' : '' + }`} + /> + {getFieldError('firstName') && ( +

{getFieldError('firstName')}

+ )} +
+
+ setLastName(e.target.value)} + className={`w-full px-6 py-6 bg-[#f0f5fc] rounded-[20px] text-[#00293d] text-sm font-light placeholder:text-[#00293d] focus:outline-none focus:ring-2 focus:ring-[#00293d]/20 transition-all ${ + getFieldError('lastName') ? 'ring-2 ring-red-400' : '' + }`} + /> + {getFieldError('lastName') && ( +

{getFieldError('lastName')}

+ )} +
{/* Email Input */} @@ -139,36 +175,51 @@ export default function SignUpPage() { value={email} onChange={(e) => setEmail(e.target.value)} required - className="w-full px-6 py-6 bg-[#f0f5fc] rounded-[20px] text-[#00293d] text-sm font-light placeholder:text-[#00293d] focus:outline-none focus:ring-2 focus:ring-[#00293d]/20 transition-all" + className={`w-full px-6 py-6 bg-[#f0f5fc] rounded-[20px] text-[#00293d] text-sm font-light placeholder:text-[#00293d] focus:outline-none focus:ring-2 focus:ring-[#00293d]/20 transition-all ${ + getFieldError('email') ? 'ring-2 ring-red-400' : '' + }`} /> + {getFieldError('email') && ( +

{getFieldError('email')}

+ )}
{/* Password Input */} -
- setPassword(e.target.value)} - required - className="w-full px-6 py-6 bg-[#f0f5fc] rounded-[20px] text-[#00293d] text-sm font-light placeholder:text-[#00293d] focus:outline-none focus:ring-2 focus:ring-[#00293d]/20 transition-all pr-14" - /> - +
+
+ setPassword(e.target.value)} + required + className={`w-full px-6 py-6 bg-[#f0f5fc] rounded-[20px] text-[#00293d] text-sm font-light placeholder:text-[#00293d] focus:outline-none focus:ring-2 focus:ring-[#00293d]/20 transition-all pr-14 ${ + getFieldError('password') ? 'ring-2 ring-red-400' : '' + }`} + /> + +
+ {getFieldError('password') && ( +

{getFieldError('password')}

+ )} +

+ Min 8 chars, 1 uppercase, 1 lowercase, 1 number, 1 special character +

{/* Sign Up Button */} diff --git a/src/services/api.ts b/src/services/api.ts new file mode 100644 index 0000000..e06593a --- /dev/null +++ b/src/services/api.ts @@ -0,0 +1,71 @@ +import axios, { AxiosError, AxiosInstance, InternalAxiosRequestConfig } from 'axios'; + +// API Response types +export interface ApiResponse { + success: boolean; + data: T; + statusCode?: number; + timestamp?: string; +} + +export interface ApiErrorResponse { + success: false; + statusCode: number; + timestamp: string; + path: string; + method: string; + error: string; + message: string | string[]; + errors?: Array<{ field: string; errors: string[] }>; +} + +// Create axios instance +const api: AxiosInstance = axios.create({ + baseURL: process.env.NEXT_PUBLIC_API_URL || 'http://localhost:3001/api/v1', + timeout: 30000, + headers: { + 'Content-Type': 'application/json', + }, +}); + +// Request interceptor +api.interceptors.request.use( + (config: InternalAxiosRequestConfig) => { + // Add auth token if available + if (typeof window !== 'undefined') { + const token = localStorage.getItem('accessToken'); + if (token && config.headers) { + config.headers.Authorization = `Bearer ${token}`; + } + } + return config; + }, + (error: AxiosError) => { + return Promise.reject(error); + } +); + +// Response interceptor +api.interceptors.response.use( + (response) => { + return response; + }, + (error: AxiosError) => { + // Handle common errors + if (error.response) { + const { status } = error.response; + + // Handle 401 Unauthorized + if (status === 401) { + if (typeof window !== 'undefined') { + localStorage.removeItem('accessToken'); + localStorage.removeItem('refreshToken'); + } + } + } + + return Promise.reject(error); + } +); + +export default api; diff --git a/src/services/auth.service.ts b/src/services/auth.service.ts new file mode 100644 index 0000000..36b734f --- /dev/null +++ b/src/services/auth.service.ts @@ -0,0 +1,146 @@ +import api, { ApiResponse, ApiErrorResponse } from './api'; +import { AxiosError } from 'axios'; + +// Types +export interface RegisterRequest { + firstName: string; + lastName?: string; + email: string; + password: string; + role: 'USER' | 'AGENT'; +} + +export interface LoginRequest { + email: string; + password: string; +} + +export interface AuthUser { + id: string; + email: string; + role: string; + firstName?: string; + lastName?: string; + avatar?: string; +} + +export interface AuthResponse { + user: AuthUser; + accessToken: string; + refreshToken: string; +} + +export interface ForgotPasswordRequest { + email: string; +} + +export interface ResetPasswordRequest { + token: string; + password: string; +} + +export interface VerifyEmailRequest { + token: string; +} + +export interface MessageResponse { + message: string; +} + +// Auth Service +class AuthService { + private basePath = '/auth'; + + async register(data: RegisterRequest): Promise> { + const response = await api.post>( + `${this.basePath}/register`, + data + ); + return response.data; + } + + async login(data: LoginRequest): Promise> { + const response = await api.post>( + `${this.basePath}/login`, + data + ); + return response.data; + } + + async forgotPassword(data: ForgotPasswordRequest): Promise> { + const response = await api.post>( + `${this.basePath}/forgot-password`, + data + ); + return response.data; + } + + async resetPassword(data: ResetPasswordRequest): Promise> { + const response = await api.post>( + `${this.basePath}/reset-password`, + data + ); + return response.data; + } + + async verifyEmail(data: VerifyEmailRequest): Promise> { + const response = await api.post>( + `${this.basePath}/verify-email`, + data + ); + return response.data; + } + + async resendVerification(): Promise> { + const response = await api.post>( + `${this.basePath}/resend-verification` + ); + return response.data; + } + + async getCurrentUser(): Promise> { + const response = await api.get>(`${this.basePath}/me`); + return response.data; + } + + async refreshToken(refreshToken: string): Promise> { + const response = await api.post>( + `${this.basePath}/refresh`, + { refreshToken } + ); + return response.data; + } + + async logout(): Promise> { + const response = await api.post>(`${this.basePath}/logout`); + return response.data; + } + + // Helper to extract error message from API error + static getErrorMessage(error: unknown): string { + if (error instanceof AxiosError) { + const apiError = error.response?.data as ApiErrorResponse; + if (apiError?.message) { + return Array.isArray(apiError.message) + ? apiError.message.join(', ') + : apiError.message; + } + } + if (error instanceof Error) { + return error.message; + } + return 'An unexpected error occurred'; + } + + // Helper to extract field errors + static getFieldErrors(error: unknown): Array<{ field: string; errors: string[] }> { + if (error instanceof AxiosError) { + const apiError = error.response?.data as ApiErrorResponse; + return apiError?.errors || []; + } + return []; + } +} + +export const authService = new AuthService(); +export { AuthService }; diff --git a/src/services/index.ts b/src/services/index.ts new file mode 100644 index 0000000..8265f0e --- /dev/null +++ b/src/services/index.ts @@ -0,0 +1,16 @@ +// API Client +export { default as api } from './api'; +export type { ApiResponse, ApiErrorResponse } from './api'; + +// Services +export { authService, AuthService } from './auth.service'; +export type { + RegisterRequest, + LoginRequest, + AuthUser, + AuthResponse, + ForgotPasswordRequest, + ResetPasswordRequest, + VerifyEmailRequest, + MessageResponse, +} from './auth.service';