From e708053eac0d4789477b5cdec10645a4b68874d7 Mon Sep 17 00:00:00 2001 From: Volodymyr Vreshch Date: Fri, 21 Nov 2025 22:29:54 +0100 Subject: [PATCH 1/4] feat: add ui user auth --- .env.example | 6 + docs/auth-setup.md | 38 ++- .../frontend/src/app/auth/callback/page.tsx | 52 +++++ .../src/components/auth/LoginButton.tsx | 66 ++++++ .../src/components/auth/LoginModal.tsx | 74 ++++++ .../frontend/src/components/auth/UserMenu.tsx | 103 +++++++++ .../frontend/src/components/auth/index.ts | 3 + .../frontend/src/components/layout/Header.tsx | 60 +++++ packages/frontend/src/lib/auth-api.ts | 218 ++++++++++++++++++ packages/frontend/src/lib/hooks/index.ts | 1 + packages/frontend/src/lib/hooks/useAuth.ts | 45 ++++ 11 files changed, 664 insertions(+), 2 deletions(-) create mode 100644 packages/frontend/src/app/auth/callback/page.tsx create mode 100644 packages/frontend/src/components/auth/LoginButton.tsx create mode 100644 packages/frontend/src/components/auth/LoginModal.tsx create mode 100644 packages/frontend/src/components/auth/UserMenu.tsx create mode 100644 packages/frontend/src/components/auth/index.ts create mode 100644 packages/frontend/src/lib/auth-api.ts create mode 100644 packages/frontend/src/lib/hooks/index.ts create mode 100644 packages/frontend/src/lib/hooks/useAuth.ts diff --git a/.env.example b/.env.example index 5b420f9..44a078a 100644 --- a/.env.example +++ b/.env.example @@ -32,3 +32,9 @@ GOOGLE_CLIENT_ID=your-google-client-id GOOGLE_CLIENT_SECRET=your-google-client-secret GOOGLE_CALLBACK_URL=http://localhost:3001/api/auth/google/callback +# =================================================================== +# Microsoft OAuth Configuration (Optional) +# =================================================================== +MICROSOFT_CLIENT_ID=your-microsoft-client-id +MICROSOFT_CLIENT_SECRET=your-microsoft-client-secret +MICROSOFT_CALLBACK_URL=http://localhost:3001/api/auth/microsoft/callback diff --git a/docs/auth-setup.md b/docs/auth-setup.md index c16b52d..86bb380 100644 --- a/docs/auth-setup.md +++ b/docs/auth-setup.md @@ -86,7 +86,29 @@ GOOGLE_CLIENT_SECRET=your-google-client-secret-here GOOGLE_CALLBACK_URL=http://localhost:3001/api/auth/google/callback ``` -### 5. Build and Start the Server +### 5. Set Up Microsoft OAuth (Optional) + +1. Go to https://portal.azure.com +2. Navigate to "Azure Active Directory" → "App registrations" +3. Click "New registration" +4. Fill in the details: + - **Name**: Agentage (Development) + - **Supported account types**: Select "Accounts in any organizational directory and personal Microsoft accounts" + - **Redirect URI**: Select "Web" and enter `http://localhost:3001/api/auth/microsoft/callback` +5. Click "Register" +6. Copy the **Application (client) ID** +7. Go to "Certificates & secrets" → "New client secret" +8. Add a description and expiration period, then click "Add" +9. Copy the **Client Secret** value (you won't be able to see it again) +10. Add to `.env`: + +```bash +MICROSOFT_CLIENT_ID=your-microsoft-client-id-here +MICROSOFT_CLIENT_SECRET=your-microsoft-client-secret-here +MICROSOFT_CALLBACK_URL=http://localhost:3001/api/auth/microsoft/callback +``` + +### 6. Build and Start the Server ```bash # Build shared package @@ -106,6 +128,8 @@ npm run dev - `GET /api/auth/github/callback` - GitHub OAuth callback - `GET /api/auth/google` - Initiate Google OAuth login - `GET /api/auth/google/callback` - Google OAuth callback +- `GET /api/auth/microsoft` - Initiate Microsoft OAuth login +- `GET /api/auth/microsoft/callback` - Microsoft OAuth callback - `POST /api/auth/logout` - Logout (client-side token removal) ### Protected Endpoints (Require JWT Token) @@ -226,7 +250,7 @@ The JWT token contains: 1. **Use HTTPS**: Always use HTTPS in production 2. **Strong JWT Secret**: Use a random 32+ character secret 3. **Update Callback URLs**: Update OAuth callback URLs to production domain -4. **Rotate Secrets**: Periodically rotate JWT secret +4. **Rotate Secrets**: Periodically rotate JWT secret and OAuth client secrets 5. **Monitor Failed Attempts**: Log and monitor failed authentication attempts ### Environment-Specific Configurations @@ -234,10 +258,14 @@ The JWT token contains: ```bash # Development GITHUB_CALLBACK_URL=http://localhost:3001/api/auth/github/callback +GOOGLE_CALLBACK_URL=http://localhost:3001/api/auth/google/callback +MICROSOFT_CALLBACK_URL=http://localhost:3001/api/auth/microsoft/callback FRONTEND_FQDN=localhost:3000 # Production GITHUB_CALLBACK_URL=https://api.agentage.io/api/auth/github/callback +GOOGLE_CALLBACK_URL=https://api.agentage.io/api/auth/google/callback +MICROSOFT_CALLBACK_URL=https://api.agentage.io/api/auth/microsoft/callback FRONTEND_FQDN=agentage.io ``` @@ -293,6 +321,11 @@ The authentication system creates a `users` collection with this structure: id: "67890", email: "user@example.com", connectedAt: Date + }, + microsoft: { + id: "abcdef", + email: "user@example.com", + connectedAt: Date } }, createdAt: Date, @@ -307,6 +340,7 @@ The following indexes are automatically created: - `email` (unique) - `providers.github.id` (unique, sparse) - `providers.google.id` (unique, sparse) +- `providers.microsoft.id` (unique, sparse) - `role` - `isActive` diff --git a/packages/frontend/src/app/auth/callback/page.tsx b/packages/frontend/src/app/auth/callback/page.tsx new file mode 100644 index 0000000..0118f4d --- /dev/null +++ b/packages/frontend/src/app/auth/callback/page.tsx @@ -0,0 +1,52 @@ +'use client'; + +import { useRouter, useSearchParams } from 'next/navigation'; +import { Suspense, useEffect } from 'react'; +import { authApi } from '../../../lib/auth-api'; + +function AuthCallbackContent() { + const router = useRouter(); + const searchParams = useSearchParams(); + + useEffect(() => { + // Extract token from URL query parameter + const token = searchParams.get('token'); + + if (token) { + // Store the JWT token + authApi.storeToken(token); + + // Redirect to home page + router.push('/'); + } else { + // No token received - redirect to home with error + router.push('/?error=auth_failed'); + } + }, [searchParams, router]); + + return ( +
+
+
+

Authenticating...

+
+
+ ); +} + +export default function AuthCallbackPage() { + return ( + +
+
+

Loading...

+
+ + } + > + +
+ ); +} diff --git a/packages/frontend/src/components/auth/LoginButton.tsx b/packages/frontend/src/components/auth/LoginButton.tsx new file mode 100644 index 0000000..9fdc56e --- /dev/null +++ b/packages/frontend/src/components/auth/LoginButton.tsx @@ -0,0 +1,66 @@ +'use client'; + +interface LoginButtonProps { + provider: 'google' | 'github' | 'microsoft'; + onClick: () => void; + className?: string; +} + +const providerConfig = { + google: { + name: 'Google', + icon: ( + + + + + + + ), + color: 'bg-white hover:bg-gray-50 text-gray-900 border border-gray-300', + }, + github: { + name: 'GitHub', + icon: ( + + + + ), + color: 'bg-gray-900 hover:bg-gray-800 text-white', + }, + microsoft: { + name: 'Microsoft', + icon: ( + + + + ), + color: 'bg-blue-600 hover:bg-blue-700 text-white', + }, +}; + +export const LoginButton = ({ provider, onClick, className = '' }: LoginButtonProps) => { + const config = providerConfig[provider]; + + return ( + + ); +}; diff --git a/packages/frontend/src/components/auth/LoginModal.tsx b/packages/frontend/src/components/auth/LoginModal.tsx new file mode 100644 index 0000000..26d0748 --- /dev/null +++ b/packages/frontend/src/components/auth/LoginModal.tsx @@ -0,0 +1,74 @@ +'use client'; + +import { LoginButton } from './LoginButton'; + +interface LoginModalProps { + isOpen: boolean; + onClose: () => void; + onLogin: (provider: 'google' | 'github' | 'microsoft') => void; +} + +export const LoginModal = ({ isOpen, onClose, onLogin }: LoginModalProps) => { + if (!isOpen) return null; + + return ( + <> + {/* Backdrop */} + diff --git a/packages/frontend/src/components/auth/UserMenu.tsx b/packages/frontend/src/components/auth/UserMenu.tsx index 0efb639..ca10330 100644 --- a/packages/frontend/src/components/auth/UserMenu.tsx +++ b/packages/frontend/src/components/auth/UserMenu.tsx @@ -43,7 +43,6 @@ export const UserMenu = ({ user, onLogout }: UserMenuProps) => { {user.name.charAt(0).toUpperCase()} )} - {user.name} {

Legal

@@ -68,14 +68,14 @@ export const Footer = () => {

Company