From 48a55e16c3e80af362c6eae23af61acf275a1f70 Mon Sep 17 00:00:00 2001 From: nilangadilhara Date: Tue, 30 Sep 2025 21:25:42 +0530 Subject: [PATCH] feat: implement clean customer authentication system with login/signup forms --- README.md | 307 +++++++++++++++++++++++++++-- package-lock.json | 202 +++++++++++++++++-- package.json | 22 ++- src/app/layout.tsx | 9 +- src/app/login/page.tsx | 10 + src/app/page.tsx | 111 ++--------- src/app/signup/page.tsx | 10 + src/components/auth/LoginForm.tsx | 125 ++++++++++++ src/components/auth/SignUpForm.tsx | 187 ++++++++++++++++++ src/context/AuthContext.tsx | 136 +++++++++++++ src/lib/api.ts | 202 +++++++++++++++++++ src/lib/types.ts | 50 +++++ src/lib/utils.ts | 38 ++++ src/lib/validations.ts | 73 +++++++ 14 files changed, 1339 insertions(+), 143 deletions(-) create mode 100644 src/app/login/page.tsx create mode 100644 src/app/signup/page.tsx create mode 100644 src/components/auth/LoginForm.tsx create mode 100644 src/components/auth/SignUpForm.tsx create mode 100644 src/context/AuthContext.tsx create mode 100644 src/lib/api.ts create mode 100644 src/lib/types.ts create mode 100644 src/lib/utils.ts create mode 100644 src/lib/validations.ts diff --git a/README.md b/README.md index e215bc4..34e88fe 100644 --- a/README.md +++ b/README.md @@ -1,36 +1,305 @@ -This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app). +# Customer Authentication Frontend + +A clean, minimalist authentication system built with Next.js 15, TypeScript, and Tailwind CSS for customer login and registration with Spring Boot backend integration. + +## Features + +### 🔐 Authentication System +- **Simple Login Form** with email/password authentication +- **Customer Registration Form** with essential customer fields (name, email, phone, date of birth) +- **JWT Token Management** with secure storage +- **Google Sign-in Integration** ready for implementation +- **Auto-redirect** based on authentication status + +### 🎨 User Interface +- **Clean Minimalist Design** with white background and bold typography +- **Full-screen Layout** with centered forms +- **Mobile-First Responsive Design** +- **Dark Gray Color Scheme** with excellent contrast +- **Smooth Loading States** and clear error messages +- **Customer-Focused UX** with intuitive navigation + +### 🛡️ Security Features +- **Input Validation** with Zod schemas +- **XSS Protection** through proper data sanitization +- **CSRF Protection** with secure cookies +- **Token Expiration** handling +- **Secure Storage** of authentication data + +## Project Structure + +``` +src/ +├── app/ # Next.js App Router +│ ├── login/ # Customer login page +│ ├── signup/ # Customer registration page +│ ├── layout.tsx # Root layout +│ └── page.tsx # Home page (redirects to login) +├── components/ +│ └── auth/ # Authentication components +│ ├── LoginForm.tsx # Clean login form with Google integration +│ └── SignUpForm.tsx # Customer registration form +├── context/ +│ └── AuthContext.tsx # Authentication state management +├── lib/ +│ ├── api.ts # API client and auth functions +│ ├── types.ts # TypeScript definitions +│ ├── utils.ts # Utility functions +│ └── validations.ts # Form validation schemas +└── styles/ # Global styles +``` ## Getting Started -First, run the development server: +### Prerequisites +- Node.js 18+ +- npm or yarn +- Spring Boot backend running on port 8080 + +### Installation + +1. **Install dependencies:** +```bash +npm install +``` + +2. **Configure environment variables:** +Create a `.env.local` file: +```env +NEXT_PUBLIC_API_URL=http://localhost:8080/api +NODE_ENV=development +``` +3. **Start the development server:** ```bash npm run dev -# or -yarn dev -# or -pnpm dev -# or -bun dev ``` -Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. +4. **Open your browser:** +Navigate to `http://localhost:3000` + +## Spring Boot Backend Integration + +### Required API Endpoints + +Your Spring Boot backend should implement these endpoints: + +#### Authentication Endpoints +```java +// POST /api/auth/login +@PostMapping("/auth/login") +public ResponseEntity login(@RequestBody LoginRequest request) { + // Validate credentials and return JWT token +} + +// POST /api/auth/signup +@PostMapping("/auth/signup") +public ResponseEntity signUp(@RequestBody SignUpRequest request) { + // Create new user account and return JWT token +} + +// POST /api/auth/logout +@PostMapping("/auth/logout") +public ResponseEntity logout() { + // Invalidate token +} + +// POST /api/auth/refresh +@PostMapping("/auth/refresh") +public ResponseEntity refreshToken(@RequestBody RefreshRequest request) { + // Refresh JWT token +} + +// GET /api/auth/me +@GetMapping("/auth/me") +public ResponseEntity getCurrentUser() { + // Return current user profile +} + +// GET /api/auth/verify +@GetMapping("/auth/verify") +public ResponseEntity verifyToken() { + // Verify token validity +} +``` + +#### Request/Response Models + +**LoginRequest:** +```java +public class LoginRequest { + private String email; + private String password; + // getters and setters +} +``` + +**SignUpRequest:** +```java +public class SignUpRequest { + private String firstName; + private String lastName; + private String email; + private String password; + private String phoneNumber; + private String dateOfBirth; + private boolean termsAccepted; + private boolean marketingEmails; + // getters and setters +} +``` + +**LoginResponse:** +```java +public class LoginResponse { + private User user; + private String token; + private String refreshToken; + private long expiresIn; // seconds + // getters and setters +} +``` + +**User:** +```java +public class User { + private String id; + private String email; + private String firstName; + private String lastName; + private String phoneNumber; + private String dateOfBirth; + private boolean isActive; + private boolean marketingEmails; + // getters and setters +} +``` + +### Spring Security Configuration + +Example Spring Security configuration: + +```java +@Configuration +@EnableWebSecurity +public class SecurityConfig { + + @Bean + public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { + http.cors().and().csrf().disable() + .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS) + .and() + .authorizeHttpRequests() + .requestMatchers("/api/auth/login", "/api/auth/refresh").permitAll() + .anyRequest().authenticated() + .and() + .addFilterBefore(jwtAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class); + + return http.build(); + } +} +``` + +## Authentication Flow + +1. **User Access**: Customer visits the application at any URL +2. **Auto-Redirect**: System automatically redirects to `/login` page +3. **Login/Register**: Customer can login or navigate to registration +4. **Form Validation**: Frontend validates input with comprehensive error handling +5. **API Integration**: Sends authentication requests to Spring Boot backend +6. **Token Management**: Securely stores JWT tokens for session management +7. **Success Handling**: Authentication context manages post-login flow +8. **Google Integration**: Ready for Google OAuth implementation + +## Key Pages + +### Login Page (`/login`) +- **Clean Design**: Large "LOGIN" title with white background +- **Simple Form**: Email and password fields with validation +- **Google Sign-in**: Ready for OAuth integration +- **Navigation**: Links to registration and password recovery + +### Registration Page (`/signup`) +- **Customer Fields**: Name, email, phone, date of birth +- **Password Confirmation**: Ensures password accuracy +- **Terms Handling**: Built-in terms acceptance +- **Seamless UX**: Easy navigation back to login + +## Security Best Practices + +### Token Management +- JWT tokens stored in secure HTTP-only cookies +- Automatic token refresh before expiration +- Proper cleanup on logout + +### Input Validation +- Client-side validation with Zod schemas +- Server-side validation in Spring Boot +- SQL injection prevention +- XSS protection + +### CORS Configuration +Ensure your Spring Boot backend allows the frontend domain: + +```java +@Configuration +public class CorsConfig { + + @Bean + public CorsConfigurationSource corsConfigurationSource() { + CorsConfiguration configuration = new CorsConfiguration(); + configuration.setAllowedOriginPatterns(Arrays.asList("http://localhost:3000")); + configuration.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "DELETE", "OPTIONS")); + configuration.setAllowedHeaders(Arrays.asList("*")); + configuration.setAllowCredentials(true); + + UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); + source.registerCorsConfiguration("/api/**", configuration); + return source; + } +} +``` + +## Customization + +### Styling +- Modify input field styling in `src/components/auth/LoginForm.tsx` and `SignUpForm.tsx` +- Update global styles in `src/app/globals.css` +- Customize colors by modifying the gray color scheme classes +- Adjust typography by changing font weights and sizes -You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file. +### Form Fields +- Add/remove customer fields in `SignUpForm.tsx` +- Update validation in `src/lib/validations.ts` +- Modify API integration in `src/context/AuthContext.tsx` -This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel. +### Google Integration +- Configure Google OAuth provider +- Update Google sign-in buttons with proper client configuration +- Add Google authentication handling in the auth context -## Learn More +### Branding +- Update page titles and descriptions +- Modify button text and messaging +- Customize error messages and user feedback -To learn more about Next.js, take a look at the following resources: +## Current Features -- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. -- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. +✅ **Clean Login Form** - Email/password with Google sign-in option +✅ **Customer Registration** - Name, email, phone, DOB fields +✅ **Input Validation** - Form validation with error handling +✅ **Responsive Design** - Mobile-first approach +✅ **Dark Gray Theme** - High contrast, accessible colors +✅ **Loading States** - Button states and error feedback +✅ **Navigation** - Seamless flow between login/signup -You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome! +## Ready for Integration -## Deploy on Vercel +🔄 **Spring Boot Backend** - API endpoints defined and ready +🔄 **Google OAuth** - UI components ready for OAuth integration +🔄 **Token Management** - JWT handling implemented +🔄 **Error Handling** - Comprehensive error states -The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. +--- -Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details. +**Customer Authentication Frontend** - Built with ❤️ using Next.js, TypeScript, and Tailwind CSS diff --git a/package-lock.json b/package-lock.json index 3ed9679..a2b5af8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,13 +8,21 @@ "name": "eda-frontend", "version": "0.1.0", "dependencies": { + "@hookform/resolvers": "^5.2.2", + "axios": "^1.12.2", + "clsx": "^2.1.1", + "js-cookie": "^3.0.5", + "lucide-react": "^0.544.0", "next": "15.5.3", "react": "19.1.0", - "react-dom": "19.1.0" + "react-dom": "19.1.0", + "react-hook-form": "^7.63.0", + "zod": "^4.1.11" }, "devDependencies": { "@eslint/eslintrc": "^3", "@tailwindcss/postcss": "^4", + "@types/js-cookie": "^3.0.6", "@types/node": "^20", "@types/react": "^19", "@types/react-dom": "^19", @@ -211,6 +219,18 @@ "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, + "node_modules/@hookform/resolvers": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/@hookform/resolvers/-/resolvers-5.2.2.tgz", + "integrity": "sha512-A/IxlMLShx3KjV/HeTcTfaMxdwy690+L/ZADoeaTltLx+CVuzkeVIPuybK3jrRfw7YZnmdKsVVHAlEPIAEUNlA==", + "license": "MIT", + "dependencies": { + "@standard-schema/utils": "^0.3.0" + }, + "peerDependencies": { + "react-hook-form": "^7.55.0" + } + }, "node_modules/@humanfs/core": { "version": "0.19.1", "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", @@ -963,6 +983,12 @@ "dev": true, "license": "MIT" }, + "node_modules/@standard-schema/utils": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@standard-schema/utils/-/utils-0.3.0.tgz", + "integrity": "sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==", + "license": "MIT" + }, "node_modules/@swc/helpers": { "version": "0.5.15", "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.15.tgz", @@ -1266,6 +1292,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", @@ -2117,6 +2150,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", @@ -2143,6 +2182,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", @@ -2207,7 +2257,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", @@ -2297,6 +2346,15 @@ "integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==", "license": "MIT" }, + "node_modules/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/color": { "version": "4.2.3", "resolved": "https://registry.npmjs.org/color/-/color-4.2.3.tgz", @@ -2342,6 +2400,18 @@ "simple-swizzle": "^0.2.2" } }, + "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", @@ -2493,6 +2563,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.0", "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.0.tgz", @@ -2520,7 +2599,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", @@ -2625,7 +2703,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" @@ -2635,7 +2712,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" @@ -2673,7 +2749,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" @@ -2686,7 +2761,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", @@ -3293,6 +3367,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", @@ -3309,11 +3403,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" @@ -3354,7 +3463,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", @@ -3379,7 +3487,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", @@ -3467,7 +3574,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" @@ -3546,7 +3652,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" @@ -3559,7 +3664,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" @@ -3575,7 +3679,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" @@ -4086,6 +4189,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", @@ -4475,6 +4587,15 @@ "loose-envify": "cli.js" } }, + "node_modules/lucide-react": { + "version": "0.544.0", + "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.544.0.tgz", + "integrity": "sha512-t5tS44bqd825zAW45UQxpG2CvcC4urOwn2TrwSH8u+MjeE+1NnWl6QqeQ/6NdjMqdOygyiT9p3Ev0p1NJykxjw==", + "license": "ISC", + "peerDependencies": { + "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, "node_modules/magic-string": { "version": "0.30.19", "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.19.tgz", @@ -4489,7 +4610,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" @@ -4519,6 +4639,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", @@ -5020,6 +5161,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", @@ -5072,6 +5219,22 @@ "react": "^19.1.0" } }, + "node_modules/react-hook-form": { + "version": "7.63.0", + "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.63.0.tgz", + "integrity": "sha512-ZwueDMvUeucovM2VjkCf7zIHcs1aAlDimZu2Hvel5C5907gUzMpm4xCrQXtRzCvsBqFjonB4m3x4LzCFI1ZKWA==", + "license": "MIT", + "engines": { + "node": ">=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/react-hook-form" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17 || ^18 || ^19" + } + }, "node_modules/react-is": { "version": "16.13.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", @@ -6134,6 +6297,15 @@ "funding": { "url": "https://github.com/sponsors/sindresorhus" } + }, + "node_modules/zod": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.1.11.tgz", + "integrity": "sha512-WPsqwxITS2tzx1bzhIKsEs19ABD5vmCVa4xBo2tq/SrV4RNZtfws1EnCWQXM6yh8bD08a1idvkB5MZSBiZsjwg==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } } } } diff --git a/package.json b/package.json index 89b65e8..c67163e 100644 --- a/package.json +++ b/package.json @@ -3,25 +3,33 @@ "version": "0.1.0", "private": true, "scripts": { - "dev": "next dev --turbopack", - "build": "next build --turbopack", + "dev": "next dev", + "build": "next build", "start": "next start", "lint": "eslint" }, "dependencies": { + "@hookform/resolvers": "^5.2.2", + "axios": "^1.12.2", + "clsx": "^2.1.1", + "js-cookie": "^3.0.5", + "lucide-react": "^0.544.0", + "next": "15.5.3", "react": "19.1.0", "react-dom": "19.1.0", - "next": "15.5.3" + "react-hook-form": "^7.63.0", + "zod": "^4.1.11" }, "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/layout.tsx b/src/app/layout.tsx index f7fa87e..81c2b75 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -1,5 +1,6 @@ import type { Metadata } from "next"; import { Geist, Geist_Mono } from "next/font/google"; +import { AuthProvider } from "@/context/AuthContext"; import "./globals.css"; const geistSans = Geist({ @@ -13,8 +14,8 @@ const geistMono = Geist_Mono({ }); export const metadata: Metadata = { - title: "Create Next App", - description: "Generated by create next app", + title: "Your Store | Online Shopping Platform", + description: "Discover amazing products and enjoy a seamless shopping experience with secure account management and personalized features.", }; export default function RootLayout({ @@ -27,7 +28,9 @@ export default function RootLayout({ - {children} + + {children} + ); diff --git a/src/app/login/page.tsx b/src/app/login/page.tsx new file mode 100644 index 0000000..11e0222 --- /dev/null +++ b/src/app/login/page.tsx @@ -0,0 +1,10 @@ +'use client'; + +import React from 'react'; +import LoginForm from '@/components/auth/LoginForm'; + +const LoginPage: React.FC = () => { + return ; +}; + +export default LoginPage; \ No newline at end of file diff --git a/src/app/page.tsx b/src/app/page.tsx index a932894..7398d03 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -1,103 +1,16 @@ -import Image from "next/image"; +'use client'; + +import { useEffect } from 'react'; +import { useRouter } from 'next/navigation'; export default function Home() { - return ( -
-
- Next.js logo -
    -
  1. - Get started by editing{" "} - - src/app/page.tsx - - . -
  2. -
  3. - Save and see your changes instantly. -
  4. -
+ const router = useRouter(); + + useEffect(() => { + // Always redirect to login page + router.push('/login'); + }, [router]); - -
- -
- ); + // Show nothing while redirecting + return null; } diff --git a/src/app/signup/page.tsx b/src/app/signup/page.tsx new file mode 100644 index 0000000..c7d35f9 --- /dev/null +++ b/src/app/signup/page.tsx @@ -0,0 +1,10 @@ +'use client'; + +import React from 'react'; +import SignUpForm from '@/components/auth/SignUpForm'; + +const SignUpPage: React.FC = () => { + return ; +}; + +export default SignUpPage; \ No newline at end of file diff --git a/src/components/auth/LoginForm.tsx b/src/components/auth/LoginForm.tsx new file mode 100644 index 0000000..d59b102 --- /dev/null +++ b/src/components/auth/LoginForm.tsx @@ -0,0 +1,125 @@ +'use client'; + +import React, { useState } from 'react'; +import Link from 'next/link'; +import { useAuth } from '@/context/AuthContext'; +import { formatErrorMessage } from '@/lib/utils'; + +const LoginForm: React.FC = () => { + const [email, setEmail] = useState(''); + const [password, setPassword] = useState(''); + const [isLoading, setIsLoading] = useState(false); + const [submitError, setSubmitError] = useState(''); + + const { login } = useAuth(); + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + setIsLoading(true); + setSubmitError(''); + + try { + await login({ email, password }); + // Redirect will be handled by the auth context + } catch (error) { + setSubmitError(formatErrorMessage(error)); + } finally { + setIsLoading(false); + } + }; + + return ( +
+
+

LOGIN

+

+ If you have an account with us, please log in. +

+ + {/* Display error message if any */} + {submitError && ( +
+ {submitError} +
+ )} + +
+ {/* Email Input */} + setEmail(e.target.value)} + required + /> + + {/* Password Input */} + setPassword(e.target.value)} + required + /> + + {/* Sign In Button */} + +
+ + {/* Social Login Options */} +
+
+
+
+
+
+ Or continue with +
+
+ +
+ +
+
+ + {/* Sign-up & Forgot Password Links */} +

+ Don't have an account?{" "} + + Create an account + + {" "}or{" "} + + Forgot your password? + +

+
+
+ ); +}; + +export default LoginForm; diff --git a/src/components/auth/SignUpForm.tsx b/src/components/auth/SignUpForm.tsx new file mode 100644 index 0000000..98275dc --- /dev/null +++ b/src/components/auth/SignUpForm.tsx @@ -0,0 +1,187 @@ +'use client'; + +import React, { useState } from 'react'; +import Link from 'next/link'; +import { useAuth } from '@/context/AuthContext'; +import { formatErrorMessage } from '@/lib/utils'; + +const SignUpForm: React.FC = () => { + const [firstName, setFirstName] = useState(''); + const [lastName, setLastName] = useState(''); + const [email, setEmail] = useState(''); + const [password, setPassword] = useState(''); + const [confirmPassword, setConfirmPassword] = useState(''); + const [phoneNumber, setPhoneNumber] = useState(''); + const [dateOfBirth, setDateOfBirth] = useState(''); + const [isLoading, setIsLoading] = useState(false); + const [submitError, setSubmitError] = useState(''); + + const { signUp } = useAuth(); + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + + if (password !== confirmPassword) { + setSubmitError('Passwords do not match'); + return; + } + + setIsLoading(true); + setSubmitError(''); + + try { + await signUp({ + firstName, + lastName, + email, + password, + phoneNumber, + dateOfBirth, + termsAccepted: true, + marketingEmails: false, + }); + // Redirect will be handled by the auth context + } catch (error) { + setSubmitError(formatErrorMessage(error)); + } finally { + setIsLoading(false); + } + }; + + return ( +
+
+

SIGN UP

+

+ Create your account to get started. +

+ + {/* Display error message if any */} + {submitError && ( +
+ {submitError} +
+ )} + +
+ {/* Name Fields */} +
+ setFirstName(e.target.value)} + required + /> + setLastName(e.target.value)} + required + /> +
+ + {/* Email Input */} + setEmail(e.target.value)} + required + /> + + {/* Phone Number */} + setPhoneNumber(e.target.value)} + required + /> + + {/* Date of Birth */} + setDateOfBirth(e.target.value)} + required + /> + + {/* Password Inputs */} + setPassword(e.target.value)} + required + /> + + setConfirmPassword(e.target.value)} + required + /> + + {/* Sign Up Button */} + +
+ + {/* Social Login Options */} +
+
+
+
+
+
+ Or continue with +
+
+ +
+ +
+
+ + {/* Sign-in Link */} +

+ Already have an account?{" "} + + Sign in + +

+
+
+ ); +}; + +export default SignUpForm; \ No newline at end of file diff --git a/src/context/AuthContext.tsx b/src/context/AuthContext.tsx new file mode 100644 index 0000000..fac24ca --- /dev/null +++ b/src/context/AuthContext.tsx @@ -0,0 +1,136 @@ +'use client'; + +import React, { createContext, useContext, useEffect, useState, ReactNode } from 'react'; +import { User, AuthContextType, LoginRequest, SignUpRequest } from '@/lib/types'; +import { authApi, getStoredUser, getStoredToken, clearAuthData } from '@/lib/api'; +import { isTokenExpired, STORAGE_KEYS } from '@/lib/utils'; +import Cookies from 'js-cookie'; + +const AuthContext = createContext(undefined); + +export const useAuth = (): AuthContextType => { + const context = useContext(AuthContext); + if (context === undefined) { + throw new Error('useAuth must be used within an AuthProvider'); + } + return context; +}; + +interface AuthProviderProps { + children: ReactNode; +} + +export const AuthProvider: React.FC = ({ children }) => { + const [user, setUser] = useState(null); + const [token, setToken] = useState(null); + const [loading, setLoading] = useState(true); + + // Initialize auth state from storage + useEffect(() => { + const initializeAuth = async () => { + try { + const storedToken = getStoredToken(); + const storedUser = getStoredUser(); + const tokenExpires = Cookies.get(STORAGE_KEYS.TOKEN_EXPIRES); + + if (storedToken && storedUser && tokenExpires) { + // Check if token is expired + if (isTokenExpired(parseInt(tokenExpires))) { + // Try to refresh token + try { + await authApi.refreshToken(); + const newToken = getStoredToken(); + const newUser = getStoredUser(); + setToken(newToken); + setUser(newUser); + } catch { + // Refresh failed, clear auth data + clearAuthData(); + setToken(null); + setUser(null); + } + } else { + // Token is still valid + setToken(storedToken); + setUser(storedUser); + } + } + } catch (error) { + console.error('Error initializing auth:', error); + clearAuthData(); + setToken(null); + setUser(null); + } finally { + setLoading(false); + } + }; + + initializeAuth(); + }, []); + + const login = async (credentials: LoginRequest): Promise => { + try { + setLoading(true); + const response = await authApi.login(credentials); + + setUser(response.user); + setToken(response.token); + } catch (error) { + setUser(null); + setToken(null); + throw error; + } finally { + setLoading(false); + } + }; + + const signUp = async (userData: SignUpRequest): Promise => { + try { + setLoading(true); + const response = await authApi.signUp(userData); + + setUser(response.user); + setToken(response.token); + } catch (error) { + setUser(null); + setToken(null); + throw error; + } finally { + setLoading(false); + } + }; + + const logout = async (): Promise => { + try { + setLoading(true); + await authApi.logout(); + } catch (error) { + console.error('Logout error:', error); + } finally { + setUser(null); + setToken(null); + setLoading(false); + + // Redirect to login page + window.location.href = '/login'; + } + }; + + const isAuthenticated = !!(user && token); + + const contextValue: AuthContextType = { + user, + token, + loading, + login, + signUp, + logout, + isAuthenticated, + }; + + return ( + + {children} + + ); +}; \ No newline at end of file diff --git a/src/lib/api.ts b/src/lib/api.ts new file mode 100644 index 0000000..65fbde6 --- /dev/null +++ b/src/lib/api.ts @@ -0,0 +1,202 @@ +import axios, { AxiosInstance, AxiosResponse } from 'axios'; +import Cookies from 'js-cookie'; +import { LoginRequest, SignUpRequest, LoginResponse, User, ApiError } from './types'; +import { STORAGE_KEYS } from './utils'; + +// Create axios instance with base configuration +const createApiClient = (): AxiosInstance => { + const client = axios.create({ + baseURL: process.env.NEXT_PUBLIC_API_URL || 'http://localhost:8080/api', + timeout: 10000, + headers: { + 'Content-Type': 'application/json', + }, + }); + + // Request interceptor to add auth token + client.interceptors.request.use( + (config) => { + const token = Cookies.get(STORAGE_KEYS.TOKEN); + if (token) { + config.headers.Authorization = `Bearer ${token}`; + } + return config; + }, + (error) => { + return Promise.reject(error); + } + ); + + // Response interceptor to handle errors + client.interceptors.response.use( + (response) => response, + (error) => { + if (error.response?.status === 401) { + // Token expired or invalid - clear auth data + clearAuthData(); + window.location.href = '/login'; + } + return Promise.reject(error); + } + ); + + return client; +}; + +const apiClient = createApiClient(); + +// Clear authentication data from storage +const clearAuthData = (): void => { + Cookies.remove(STORAGE_KEYS.TOKEN); + Cookies.remove(STORAGE_KEYS.REFRESH_TOKEN); + Cookies.remove(STORAGE_KEYS.USER); + Cookies.remove(STORAGE_KEYS.TOKEN_EXPIRES); +}; + +// Save authentication data to storage +const saveAuthData = (loginResponse: LoginResponse): void => { + const expiresAt = Date.now() + (loginResponse.expiresIn * 1000); + + Cookies.set(STORAGE_KEYS.TOKEN, loginResponse.token, { + expires: 7, // 7 days + secure: process.env.NODE_ENV === 'production', + sameSite: 'strict' + }); + + Cookies.set(STORAGE_KEYS.REFRESH_TOKEN, loginResponse.refreshToken, { + expires: 30, // 30 days + secure: process.env.NODE_ENV === 'production', + sameSite: 'strict' + }); + + Cookies.set(STORAGE_KEYS.USER, JSON.stringify(loginResponse.user), { + expires: 7, + secure: process.env.NODE_ENV === 'production', + sameSite: 'strict' + }); + + Cookies.set(STORAGE_KEYS.TOKEN_EXPIRES, expiresAt.toString(), { + expires: 7, + secure: process.env.NODE_ENV === 'production', + sameSite: 'strict' + }); +}; + +// Get stored user data +const getStoredUser = (): User | null => { + try { + const userStr = Cookies.get(STORAGE_KEYS.USER); + return userStr ? JSON.parse(userStr) : null; + } catch { + return null; + } +}; + +// Get stored token +const getStoredToken = (): string | null => { + return Cookies.get(STORAGE_KEYS.TOKEN) || null; +}; + +// Authentication API functions +export const authApi = { + // Login user + async login(credentials: LoginRequest): Promise { + try { + const response: AxiosResponse = await apiClient.post( + '/auth/login', + credentials + ); + + // Save auth data to storage + saveAuthData(response.data); + + return response.data; + } catch (error: any) { + throw new Error( + error.response?.data?.message || 'Login failed. Please check your credentials.' + ); + } + }, + + // Sign up user + async signUp(userData: SignUpRequest): Promise { + try { + const response: AxiosResponse = await apiClient.post( + '/auth/register', + userData + ); + + // Save auth data to storage (auto-login after signup) + saveAuthData(response.data); + + return response.data; + } catch (error: any) { + throw new Error( + error.response?.data?.message || 'Registration failed. Please try again.' + ); + } + }, + + // Logout user + async logout(): Promise { + try { + await apiClient.post('/auth/logout'); + } catch (error) { + // Continue with logout even if API call fails + console.warn('Logout API call failed:', error); + } finally { + clearAuthData(); + } + }, + + // Refresh token + async refreshToken(): Promise { + try { + const refreshToken = Cookies.get(STORAGE_KEYS.REFRESH_TOKEN); + if (!refreshToken) { + throw new Error('No refresh token available'); + } + + const response: AxiosResponse = await apiClient.post( + '/auth/refresh', + { refreshToken } + ); + + saveAuthData(response.data); + return response.data; + } catch (error: any) { + clearAuthData(); + throw new Error( + error.response?.data?.message || 'Failed to refresh token' + ); + } + }, + + // Get current user profile + async getCurrentUser(): Promise { + try { + const response: AxiosResponse = await apiClient.get('/auth/me'); + return response.data; + } catch (error: any) { + throw new Error( + error.response?.data?.message || 'Failed to get user profile' + ); + } + }, + + // Verify token validity + async verifyToken(): Promise { + try { + await apiClient.get('/auth/verify'); + return true; + } catch { + return false; + } + }, +}; + +// Export utility functions +export { getStoredUser, getStoredToken, clearAuthData }; + +// Export configured axios instance for other API calls +export { apiClient }; \ No newline at end of file diff --git a/src/lib/types.ts b/src/lib/types.ts new file mode 100644 index 0000000..986de60 --- /dev/null +++ b/src/lib/types.ts @@ -0,0 +1,50 @@ +export interface User { + id: string; + email: string; + firstName: string; + lastName: string; + phoneNumber: string; + dateOfBirth: string; + isActive: boolean; + isVerified: boolean; + marketingEmails: boolean; + createdAt: string; +} + +export interface LoginRequest { + email: string; + password: string; +} + +export interface SignUpRequest { + firstName: string; + lastName: string; + email: string; + password: string; + phoneNumber: string; + dateOfBirth: string; + marketingEmails: boolean; +} + +export interface LoginResponse { + user: User; + token: string; + refreshToken: string; + expiresIn: number; +} + +export interface ApiError { + message: string; + code: string; + details?: any; +} + +export interface AuthContextType { + user: User | null; + token: string | null; + loading: boolean; + login: (credentials: LoginRequest) => Promise; + signUp: (userData: SignUpRequest) => Promise; + logout: () => void; + isAuthenticated: boolean; +} \ No newline at end of file diff --git a/src/lib/utils.ts b/src/lib/utils.ts new file mode 100644 index 0000000..2bfe6fe --- /dev/null +++ b/src/lib/utils.ts @@ -0,0 +1,38 @@ +import { type ClassValue, clsx } from "clsx"; + +// Utility function for conditional class names (similar to clsx but simpler) +export function cn(...inputs: ClassValue[]) { + return clsx(inputs); +} + +// Format user display name +export function formatUserName(firstName: string, lastName: string): string { + return `${firstName} ${lastName}`.trim(); +} + +// Validate email format +export function isValidEmail(email: string): boolean { + const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; + return emailRegex.test(email); +} + +// Format error message for display +export function formatErrorMessage(error: any): string { + if (typeof error === 'string') return error; + if (error?.response?.data?.message) return error.response.data.message; + if (error?.message) return error.message; + return 'An unexpected error occurred. Please try again.'; +} + +// Check if token is expired +export function isTokenExpired(expirationTime: number): boolean { + return Date.now() >= expirationTime * 1000; +} + +// Storage keys +export const STORAGE_KEYS = { + TOKEN: 'auth_token', + REFRESH_TOKEN: 'refresh_token', + USER: 'auth_user', + TOKEN_EXPIRES: 'token_expires', +} as const; \ No newline at end of file diff --git a/src/lib/validations.ts b/src/lib/validations.ts new file mode 100644 index 0000000..5696565 --- /dev/null +++ b/src/lib/validations.ts @@ -0,0 +1,73 @@ +import { z } from 'zod'; + +export const loginSchema = z.object({ + email: z + .string() + .min(1, 'Email is required') + .email('Please enter a valid email address') + .max(100, 'Email must be less than 100 characters'), + password: z + .string() + .min(1, 'Password is required') + .min(6, 'Password must be at least 6 characters') + .max(100, 'Password must be less than 100 characters'), +}); + +export const signUpSchema = z.object({ + firstName: z + .string() + .min(1, 'First name is required') + .min(2, 'First name must be at least 2 characters') + .max(50, 'First name must be less than 50 characters') + .regex(/^[a-zA-Z\s]+$/, 'First name can only contain letters and spaces'), + lastName: z + .string() + .min(1, 'Last name is required') + .min(2, 'Last name must be at least 2 characters') + .max(50, 'Last name must be less than 50 characters') + .regex(/^[a-zA-Z\s]+$/, 'Last name can only contain letters and spaces'), + email: z + .string() + .min(1, 'Email is required') + .email('Please enter a valid email address') + .max(100, 'Email must be less than 100 characters'), + password: z + .string() + .min(1, 'Password is required') + .min(8, 'Password must be at least 8 characters') + .max(100, 'Password must be less than 100 characters') + .regex(/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]/, + 'Password must contain at least one uppercase letter, one lowercase letter, one number, and one special character'), + confirmPassword: z + .string() + .min(1, 'Please confirm your password'), + phoneNumber: z + .string() + .min(1, 'Phone number is required') + .regex(/^\+?[\d\s\-\(\)]+$/, 'Please enter a valid phone number') + .min(10, 'Phone number must be at least 10 digits') + .max(20, 'Phone number must be less than 20 characters'), + dateOfBirth: z + .string() + .min(1, 'Date of birth is required') + .refine((date) => { + const birthDate = new Date(date); + const today = new Date(); + const age = today.getFullYear() - birthDate.getFullYear(); + return age >= 18; + }, 'You must be at least 18 years old'), + termsAccepted: z + .boolean() + .refine((val) => val === true, { + message: 'You must accept the terms and conditions', + }), + marketingEmails: z + .boolean() + .optional(), +}).refine((data) => data.password === data.confirmPassword, { + message: "Passwords don't match", + path: ["confirmPassword"], +}); + +export type LoginFormData = z.infer; +export type SignUpFormData = z.infer; \ No newline at end of file