@sfdc-webapps/feature-authentication 1.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +97 -0
- package/feature.ts +14 -0
- package/package.json +24 -0
- package/template/webApp/src/__inherit__appLayout.tsx +9 -0
- package/template/webApp/src/app.tsx +17 -0
- package/template/webApp/src/components/AuthError.tsx +20 -0
- package/template/webApp/src/components/PrivateRoute.tsx +19 -0
- package/template/webApp/src/context/AuthContext.tsx +64 -0
- package/template/webApp/src/pages/ForgotPassword.tsx +39 -0
- package/template/webApp/src/pages/Login.tsx +57 -0
- package/template/webApp/src/pages/Profile.tsx +37 -0
- package/template/webApp/src/pages/Register.tsx +69 -0
- package/template/webApp/src/pages/ResetPassword.tsx +49 -0
- package/template/webApp/src/routes.tsx +48 -0
- package/template/webApp/webapp.json +12 -0
- package/tsconfig.app.json +28 -0
- package/tsconfig.json +7 -0
- package/tsconfig.node.json +26 -0
package/README.md
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
# Feature: Authentication
|
|
2
|
+
|
|
3
|
+
This feature provides authentication functionality for Salesforce Web Applications, including:
|
|
4
|
+
|
|
5
|
+
- **AuthContext** - React context for managing authentication state
|
|
6
|
+
- **PrivateRoute** - Route protection component for authenticated-only pages
|
|
7
|
+
- **Authentication Pages** - Login, Register, Profile, Forgot Password, Reset Password
|
|
8
|
+
|
|
9
|
+
## Features
|
|
10
|
+
|
|
11
|
+
- Checks authentication status via Salesforce Chatter API (`/chatter/users/me`)
|
|
12
|
+
- Protects routes with automatic redirect to login
|
|
13
|
+
- Supports return URL (`retUrl`) query parameter for post-login navigation
|
|
14
|
+
- Displays user profile information from Salesforce
|
|
15
|
+
|
|
16
|
+
## Setup
|
|
17
|
+
|
|
18
|
+
### Prerequisites
|
|
19
|
+
|
|
20
|
+
1. **Salesforce CLI (`sf`)** - Must be installed and authenticated to a Salesforce org
|
|
21
|
+
2. **@salesforce/webapps packages** - Required for API proxy functionality (see linking instructions below)
|
|
22
|
+
|
|
23
|
+
### Linking @salesforce/webapps (Required until packages are published)
|
|
24
|
+
|
|
25
|
+
Since `@salesforce/webapps` and `@salesforce/vite-plugin-webapps` are not yet published to npm, you need to link them locally:
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
# Step 1: In the webapps repo - register packages with yarn's link registry
|
|
29
|
+
cd /path/to/webapps/packages/webapps && yarn link
|
|
30
|
+
cd /path/to/webapps/packages/vite-plugin-webapps && yarn link
|
|
31
|
+
|
|
32
|
+
# Step 2: In this repo - link the packages
|
|
33
|
+
cd /path/to/webapps-templates/packages/feature-authentication
|
|
34
|
+
yarn link @salesforce/webapps
|
|
35
|
+
yarn link @salesforce/vite-plugin-webapps
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
### Building and Running
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
# Build the feature (applies patches from template to dist)
|
|
42
|
+
yarn build
|
|
43
|
+
|
|
44
|
+
# Start the development server
|
|
45
|
+
yarn dev
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
The `yarn build` command will:
|
|
49
|
+
1. Apply feature patches to create the dist directory
|
|
50
|
+
2. Remove the yarn.lock (to avoid resolution conflicts)
|
|
51
|
+
3. Re-link the @salesforce/webapps packages
|
|
52
|
+
|
|
53
|
+
## Usage
|
|
54
|
+
|
|
55
|
+
### Protecting Routes
|
|
56
|
+
|
|
57
|
+
Wrap routes that require authentication with `PrivateRoute`:
|
|
58
|
+
|
|
59
|
+
```tsx
|
|
60
|
+
import PrivateRoute from "./components/PrivateRoute";
|
|
61
|
+
|
|
62
|
+
// In routes.tsx
|
|
63
|
+
{
|
|
64
|
+
element: <PrivateRoute />,
|
|
65
|
+
children: [
|
|
66
|
+
{
|
|
67
|
+
path: 'profile',
|
|
68
|
+
element: <Profile />,
|
|
69
|
+
}
|
|
70
|
+
]
|
|
71
|
+
}
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
### Using Auth Context
|
|
75
|
+
|
|
76
|
+
Access authentication state in any component:
|
|
77
|
+
|
|
78
|
+
```tsx
|
|
79
|
+
import { useAuth } from '../context/AuthContext';
|
|
80
|
+
|
|
81
|
+
function MyComponent() {
|
|
82
|
+
const { user, isAuthenticated, loading, error } = useAuth();
|
|
83
|
+
|
|
84
|
+
if (loading) return <div>Loading...</div>;
|
|
85
|
+
if (!isAuthenticated) return <div>Please log in</div>;
|
|
86
|
+
|
|
87
|
+
return <div>Welcome, {user?.displayName}</div>;
|
|
88
|
+
}
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
## Notes
|
|
92
|
+
|
|
93
|
+
- The feature overrides `app.tsx` to wrap the app with `AuthProvider`
|
|
94
|
+
- The feature inherits `appLayout.tsx` from the base app (no override)
|
|
95
|
+
- Routes are merged with the base app's routes (Home, About are preserved)
|
|
96
|
+
- The `webapp.json` manifest is required for the Salesforce proxy to function > ***⚠️ TODO: this should be removed once they are decoupled (webapp.json manifest file should not be required)***
|
|
97
|
+
|
package/feature.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { Feature } from '../cli/src/types.js';
|
|
2
|
+
|
|
3
|
+
const feature: Feature = {
|
|
4
|
+
packageJson: {
|
|
5
|
+
dependencies: {
|
|
6
|
+
'@salesforce/webapps': '*',
|
|
7
|
+
},
|
|
8
|
+
devDependencies: {
|
|
9
|
+
'@salesforce/vite-plugin-webapps': '*',
|
|
10
|
+
},
|
|
11
|
+
},
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export default feature;
|
package/package.json
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@sfdc-webapps/feature-authentication",
|
|
3
|
+
"version": "1.0.2",
|
|
4
|
+
"description": "Authentication feature for web applications",
|
|
5
|
+
"license": "ISC",
|
|
6
|
+
"author": "",
|
|
7
|
+
"type": "module",
|
|
8
|
+
"main": "index.js",
|
|
9
|
+
"publishConfig": {
|
|
10
|
+
"access": "public"
|
|
11
|
+
},
|
|
12
|
+
"scripts": {
|
|
13
|
+
"dev": "cd dist/digitalExperiences/webApplications/feature-authentication && yarn && yarn dev",
|
|
14
|
+
"build": "npx tsx ../cli/src/index.ts apply-patches packages/feature-authentication packages/base-react-app packages/feature-authentication/dist --clean --skip-dependency-changes && yarn postbuild",
|
|
15
|
+
"postbuild": "cd dist/digitalExperiences/webApplications/feature-authentication && yarn link @salesforce/webapps && yarn link @salesforce/vite-plugin-webapps",
|
|
16
|
+
"watch": "npx tsx ../cli/src/index.ts watch-patches packages/feature-authentication packages/base-react-app packages/feature-authentication/dist --clean"
|
|
17
|
+
},
|
|
18
|
+
"devDependencies": {
|
|
19
|
+
"@types/react": "^19.2.7",
|
|
20
|
+
"@types/react-dom": "^19.2.3",
|
|
21
|
+
"react-dom": "^19.2.1",
|
|
22
|
+
"react-router": "^7.10.1"
|
|
23
|
+
}
|
|
24
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { createBrowserRouter, RouterProvider } from 'react-router';
|
|
2
|
+
import { routes } from '@/routes';
|
|
3
|
+
import { StrictMode } from 'react'
|
|
4
|
+
import { createRoot } from 'react-dom/client'
|
|
5
|
+
import { AuthProvider } from './context/AuthContext';
|
|
6
|
+
import './styles/global.css'
|
|
7
|
+
|
|
8
|
+
const router = createBrowserRouter(routes);
|
|
9
|
+
|
|
10
|
+
createRoot(document.getElementById('root')!).render(
|
|
11
|
+
<StrictMode>
|
|
12
|
+
<AuthProvider>
|
|
13
|
+
<RouterProvider router={router} />
|
|
14
|
+
</AuthProvider>
|
|
15
|
+
</StrictMode>,
|
|
16
|
+
)
|
|
17
|
+
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
interface AuthErrorProps {
|
|
2
|
+
error: string;
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
export default function AuthError({ error }: AuthErrorProps) {
|
|
6
|
+
return (
|
|
7
|
+
<div className="mb-6 p-4 bg-red-50 border border-red-200 rounded-md">
|
|
8
|
+
<div className="flex items-start gap-3">
|
|
9
|
+
<svg className="w-5 h-5 text-red-600 mt-0.5 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
10
|
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
|
11
|
+
</svg>
|
|
12
|
+
<div>
|
|
13
|
+
<p className="text-sm font-medium text-red-800">Authentication Error</p>
|
|
14
|
+
<p className="text-sm text-red-700 mt-1">{error}</p>
|
|
15
|
+
</div>
|
|
16
|
+
</div>
|
|
17
|
+
</div>
|
|
18
|
+
);
|
|
19
|
+
}
|
|
20
|
+
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { Navigate, Outlet, useLocation } from 'react-router';
|
|
2
|
+
import { useAuth } from '../context/AuthContext';
|
|
3
|
+
|
|
4
|
+
export default function PrivateRoute() {
|
|
5
|
+
const { isAuthenticated, loading } = useAuth();
|
|
6
|
+
const location = useLocation();
|
|
7
|
+
|
|
8
|
+
if (loading) {
|
|
9
|
+
return null;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
if (!isAuthenticated) {
|
|
13
|
+
// Build the return URL with the current path
|
|
14
|
+
const returnUrl = encodeURIComponent(location.pathname + location.search);
|
|
15
|
+
return <Navigate to={`/login?retUrl=${returnUrl}`} replace />;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
return <Outlet />;
|
|
19
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { createContext, useContext, useState, useEffect, useCallback, type ReactNode } from 'react';
|
|
2
|
+
import { getCurrentUser, type User } from '@salesforce/webapps/api';
|
|
3
|
+
|
|
4
|
+
interface AuthContextType {
|
|
5
|
+
user: User | null;
|
|
6
|
+
isAuthenticated: boolean;
|
|
7
|
+
loading: boolean;
|
|
8
|
+
error: string | null;
|
|
9
|
+
checkAuth: () => Promise<void>;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const AuthContext = createContext<AuthContextType | undefined>(undefined);
|
|
13
|
+
|
|
14
|
+
interface AuthProviderProps {
|
|
15
|
+
children: ReactNode;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function AuthProvider({ children }: AuthProviderProps) {
|
|
19
|
+
const [user, setUser] = useState<User | null>(null);
|
|
20
|
+
const [loading, setLoading] = useState(true);
|
|
21
|
+
const [error, setError] = useState<string | null>(null);
|
|
22
|
+
|
|
23
|
+
const checkAuth = useCallback(async () => {
|
|
24
|
+
setLoading(true);
|
|
25
|
+
setError(null);
|
|
26
|
+
|
|
27
|
+
try {
|
|
28
|
+
const userData = await getCurrentUser();
|
|
29
|
+
setUser(userData);
|
|
30
|
+
} catch (err) {
|
|
31
|
+
const errorMessage = err instanceof Error ? err.message : 'Authentication failed';
|
|
32
|
+
setError(errorMessage);
|
|
33
|
+
setUser(null);
|
|
34
|
+
} finally {
|
|
35
|
+
setLoading(false);
|
|
36
|
+
}
|
|
37
|
+
}, []);
|
|
38
|
+
|
|
39
|
+
useEffect(() => {
|
|
40
|
+
checkAuth();
|
|
41
|
+
}, [checkAuth]);
|
|
42
|
+
|
|
43
|
+
const value: AuthContextType = {
|
|
44
|
+
user,
|
|
45
|
+
isAuthenticated: user !== null,
|
|
46
|
+
loading,
|
|
47
|
+
error,
|
|
48
|
+
checkAuth,
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
return (
|
|
52
|
+
<AuthContext.Provider value={value}>
|
|
53
|
+
{children}
|
|
54
|
+
</AuthContext.Provider>
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function useAuth(): AuthContextType {
|
|
59
|
+
const context = useContext(AuthContext);
|
|
60
|
+
if (context === undefined) {
|
|
61
|
+
throw new Error('useAuth must be used within an AuthProvider');
|
|
62
|
+
}
|
|
63
|
+
return context;
|
|
64
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { Link } from "react-router";
|
|
2
|
+
|
|
3
|
+
export default function ForgotPassword() {
|
|
4
|
+
return (
|
|
5
|
+
<div className="max-w-md mx-auto p-6">
|
|
6
|
+
<div className="bg-white shadow rounded-lg p-8">
|
|
7
|
+
<h1 className="text-3xl font-bold text-center mb-4">Forgot Password</h1>
|
|
8
|
+
<p className="text-gray-600 text-center mb-6">
|
|
9
|
+
Enter your username and we'll send you a link to reset your password.
|
|
10
|
+
</p>
|
|
11
|
+
<form className="space-y-6">
|
|
12
|
+
<div>
|
|
13
|
+
<label htmlFor="username" className="block text-sm font-medium mb-2">
|
|
14
|
+
Username
|
|
15
|
+
</label>
|
|
16
|
+
<input
|
|
17
|
+
type="text"
|
|
18
|
+
id="username"
|
|
19
|
+
className="w-full px-3 py-2 border rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
|
20
|
+
placeholder="Enter your username"
|
|
21
|
+
/>
|
|
22
|
+
</div>
|
|
23
|
+
<button
|
|
24
|
+
type="submit"
|
|
25
|
+
className="w-full bg-blue-600 text-white py-2 rounded-md hover:bg-blue-700"
|
|
26
|
+
>
|
|
27
|
+
Send Reset Link
|
|
28
|
+
</button>
|
|
29
|
+
</form>
|
|
30
|
+
<p className="mt-6 text-center text-sm text-gray-600">
|
|
31
|
+
Remember your password?{' '}
|
|
32
|
+
<Link to="/login" className="text-blue-600 hover:underline">
|
|
33
|
+
Sign in
|
|
34
|
+
</Link>
|
|
35
|
+
</p>
|
|
36
|
+
</div>
|
|
37
|
+
</div>
|
|
38
|
+
);
|
|
39
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { Link } from 'react-router';
|
|
2
|
+
|
|
3
|
+
export default function Login() {
|
|
4
|
+
return (
|
|
5
|
+
<div className="max-w-md mx-auto p-6">
|
|
6
|
+
<div className="bg-white shadow rounded-lg p-8">
|
|
7
|
+
<h1 className="text-3xl font-bold text-center mb-6">Login</h1>
|
|
8
|
+
|
|
9
|
+
<form className="space-y-6">
|
|
10
|
+
<div>
|
|
11
|
+
<label htmlFor="email" className="block text-sm font-medium mb-2">
|
|
12
|
+
Email
|
|
13
|
+
</label>
|
|
14
|
+
<input
|
|
15
|
+
type="email"
|
|
16
|
+
id="email"
|
|
17
|
+
className="w-full px-3 py-2 border rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
|
18
|
+
placeholder="Enter your email"
|
|
19
|
+
/>
|
|
20
|
+
</div>
|
|
21
|
+
<div>
|
|
22
|
+
<label htmlFor="password" className="block text-sm font-medium mb-2">
|
|
23
|
+
Password
|
|
24
|
+
</label>
|
|
25
|
+
<input
|
|
26
|
+
type="password"
|
|
27
|
+
id="password"
|
|
28
|
+
className="w-full px-3 py-2 border rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
|
29
|
+
placeholder="Enter your password"
|
|
30
|
+
/>
|
|
31
|
+
</div>
|
|
32
|
+
<div className="flex items-center justify-between">
|
|
33
|
+
<label className="flex items-center gap-2 text-sm">
|
|
34
|
+
<input type="checkbox" className="rounded" />
|
|
35
|
+
Remember me
|
|
36
|
+
</label>
|
|
37
|
+
<Link to="/forgot-password" className="text-sm text-blue-600 hover:underline">
|
|
38
|
+
Forgot password?
|
|
39
|
+
</Link>
|
|
40
|
+
</div>
|
|
41
|
+
<button
|
|
42
|
+
type="submit"
|
|
43
|
+
className="w-full bg-blue-600 text-white py-2 rounded-md hover:bg-blue-700"
|
|
44
|
+
>
|
|
45
|
+
Sign In
|
|
46
|
+
</button>
|
|
47
|
+
</form>
|
|
48
|
+
<p className="mt-6 text-center text-sm text-gray-600">
|
|
49
|
+
Don't have an account?{' '}
|
|
50
|
+
<Link to="/register" className="text-blue-600 hover:underline">
|
|
51
|
+
Sign up
|
|
52
|
+
</Link>
|
|
53
|
+
</p>
|
|
54
|
+
</div>
|
|
55
|
+
</div>
|
|
56
|
+
);
|
|
57
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { useAuth } from "../context/AuthContext";
|
|
2
|
+
|
|
3
|
+
export default function Profile() {
|
|
4
|
+
const { user } = useAuth();
|
|
5
|
+
|
|
6
|
+
if (!user) {
|
|
7
|
+
return null;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
return (
|
|
11
|
+
<div className="max-w-4xl mx-auto p-6">
|
|
12
|
+
<div className="bg-white shadow rounded-lg p-8">
|
|
13
|
+
<h1 className="text-3xl font-bold mb-8">User Profile</h1>
|
|
14
|
+
|
|
15
|
+
<div className="space-y-6">
|
|
16
|
+
<section>
|
|
17
|
+
<h2 className="text-xl font-semibold mb-4">Profile Information</h2>
|
|
18
|
+
<div className="space-y-4">
|
|
19
|
+
<div>
|
|
20
|
+
<label className="block text-sm font-medium text-gray-500 mb-1">
|
|
21
|
+
User ID
|
|
22
|
+
</label>
|
|
23
|
+
<p className="text-gray-900 font-mono text-sm">{user.id}</p>
|
|
24
|
+
</div>
|
|
25
|
+
<div>
|
|
26
|
+
<label className="block text-sm font-medium text-gray-500 mb-1">
|
|
27
|
+
Name
|
|
28
|
+
</label>
|
|
29
|
+
<p className="text-gray-900">{user.name}</p>
|
|
30
|
+
</div>
|
|
31
|
+
</div>
|
|
32
|
+
</section>
|
|
33
|
+
</div>
|
|
34
|
+
</div>
|
|
35
|
+
</div>
|
|
36
|
+
);
|
|
37
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { Link } from "react-router";
|
|
2
|
+
|
|
3
|
+
export default function Register() {
|
|
4
|
+
return (
|
|
5
|
+
<div className="max-w-md mx-auto p-6">
|
|
6
|
+
<div className="bg-white shadow rounded-lg p-8">
|
|
7
|
+
<h1 className="text-3xl font-bold text-center mb-6">Create Account</h1>
|
|
8
|
+
<form className="space-y-6">
|
|
9
|
+
<div>
|
|
10
|
+
<label htmlFor="name" className="block text-sm font-medium mb-2">
|
|
11
|
+
Full Name
|
|
12
|
+
</label>
|
|
13
|
+
<input
|
|
14
|
+
type="text"
|
|
15
|
+
id="name"
|
|
16
|
+
className="w-full px-3 py-2 border rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
|
17
|
+
placeholder="Enter your full name"
|
|
18
|
+
/>
|
|
19
|
+
</div>
|
|
20
|
+
<div>
|
|
21
|
+
<label htmlFor="email" className="block text-sm font-medium mb-2">
|
|
22
|
+
Email
|
|
23
|
+
</label>
|
|
24
|
+
<input
|
|
25
|
+
type="email"
|
|
26
|
+
id="email"
|
|
27
|
+
className="w-full px-3 py-2 border rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
|
28
|
+
placeholder="Enter your email"
|
|
29
|
+
/>
|
|
30
|
+
</div>
|
|
31
|
+
<div>
|
|
32
|
+
<label htmlFor="password" className="block text-sm font-medium mb-2">
|
|
33
|
+
Password
|
|
34
|
+
</label>
|
|
35
|
+
<input
|
|
36
|
+
type="password"
|
|
37
|
+
id="password"
|
|
38
|
+
className="w-full px-3 py-2 border rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
|
39
|
+
placeholder="Create a password"
|
|
40
|
+
/>
|
|
41
|
+
</div>
|
|
42
|
+
<div>
|
|
43
|
+
<label htmlFor="confirmPassword" className="block text-sm font-medium mb-2">
|
|
44
|
+
Confirm Password
|
|
45
|
+
</label>
|
|
46
|
+
<input
|
|
47
|
+
type="password"
|
|
48
|
+
id="confirmPassword"
|
|
49
|
+
className="w-full px-3 py-2 border rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
|
50
|
+
placeholder="Confirm your password"
|
|
51
|
+
/>
|
|
52
|
+
</div>
|
|
53
|
+
<button
|
|
54
|
+
type="submit"
|
|
55
|
+
className="w-full bg-blue-600 text-white py-2 rounded-md hover:bg-blue-700"
|
|
56
|
+
>
|
|
57
|
+
Create Account
|
|
58
|
+
</button>
|
|
59
|
+
</form>
|
|
60
|
+
<p className="mt-6 text-center text-sm text-gray-600">
|
|
61
|
+
Already have an account?{' '}
|
|
62
|
+
<Link to="/login" className="text-blue-600 hover:underline">
|
|
63
|
+
Sign in
|
|
64
|
+
</Link>
|
|
65
|
+
</p>
|
|
66
|
+
</div>
|
|
67
|
+
</div>
|
|
68
|
+
);
|
|
69
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { Link } from "react-router";
|
|
2
|
+
|
|
3
|
+
export default function ResetPassword() {
|
|
4
|
+
return (
|
|
5
|
+
<div className="max-w-md mx-auto p-6">
|
|
6
|
+
<div className="bg-white shadow rounded-lg p-8">
|
|
7
|
+
<h1 className="text-3xl font-bold text-center mb-4">Reset Password</h1>
|
|
8
|
+
<p className="text-gray-600 text-center mb-6">
|
|
9
|
+
Enter your new password below.
|
|
10
|
+
</p>
|
|
11
|
+
<form className="space-y-6">
|
|
12
|
+
<div>
|
|
13
|
+
<label htmlFor="password" className="block text-sm font-medium mb-2">
|
|
14
|
+
New Password
|
|
15
|
+
</label>
|
|
16
|
+
<input
|
|
17
|
+
type="password"
|
|
18
|
+
id="password"
|
|
19
|
+
className="w-full px-3 py-2 border rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
|
20
|
+
placeholder="Enter new password"
|
|
21
|
+
/>
|
|
22
|
+
</div>
|
|
23
|
+
<div>
|
|
24
|
+
<label htmlFor="confirmPassword" className="block text-sm font-medium mb-2">
|
|
25
|
+
Confirm New Password
|
|
26
|
+
</label>
|
|
27
|
+
<input
|
|
28
|
+
type="password"
|
|
29
|
+
id="confirmPassword"
|
|
30
|
+
className="w-full px-3 py-2 border rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
|
31
|
+
placeholder="Confirm new password"
|
|
32
|
+
/>
|
|
33
|
+
</div>
|
|
34
|
+
<button
|
|
35
|
+
type="submit"
|
|
36
|
+
className="w-full bg-blue-600 text-white py-2 rounded-md hover:bg-blue-700"
|
|
37
|
+
>
|
|
38
|
+
Reset Password
|
|
39
|
+
</button>
|
|
40
|
+
</form>
|
|
41
|
+
<p className="mt-6 text-center text-sm text-gray-600">
|
|
42
|
+
<Link to="/login" className="text-blue-600 hover:underline">
|
|
43
|
+
Back to Sign in
|
|
44
|
+
</Link>
|
|
45
|
+
</p>
|
|
46
|
+
</div>
|
|
47
|
+
</div>
|
|
48
|
+
);
|
|
49
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import type { RouteObject } from "react-router";
|
|
2
|
+
import AppLayout from "./__inherit__appLayout";
|
|
3
|
+
import Login from "./pages/Login";
|
|
4
|
+
import Register from "./pages/Register";
|
|
5
|
+
import ForgotPassword from "./pages/ForgotPassword";
|
|
6
|
+
import ResetPassword from "./pages/ResetPassword";
|
|
7
|
+
import Profile from "./pages/Profile";
|
|
8
|
+
import PrivateRoute from "./components/PrivateRoute";
|
|
9
|
+
|
|
10
|
+
export const routes: RouteObject[] = [
|
|
11
|
+
{
|
|
12
|
+
path: '/',
|
|
13
|
+
element: <AppLayout />,
|
|
14
|
+
children: [
|
|
15
|
+
{
|
|
16
|
+
path: 'login',
|
|
17
|
+
element: <Login />,
|
|
18
|
+
handle: { showInNavigation: true, label: 'Login' }
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
path: 'register',
|
|
22
|
+
element: <Register />,
|
|
23
|
+
handle: { showInNavigation: false }
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
path: 'forgot-password',
|
|
27
|
+
element: <ForgotPassword />,
|
|
28
|
+
handle: { showInNavigation: false }
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
path: 'reset-password',
|
|
32
|
+
element: <ResetPassword />,
|
|
33
|
+
handle: { showInNavigation: false }
|
|
34
|
+
},
|
|
35
|
+
// Protected routes - requires authentication
|
|
36
|
+
{
|
|
37
|
+
element: <PrivateRoute />,
|
|
38
|
+
children: [
|
|
39
|
+
{
|
|
40
|
+
path: 'profile',
|
|
41
|
+
element: <Profile />,
|
|
42
|
+
handle: { showInNavigation: true, label: 'Profile' }
|
|
43
|
+
}
|
|
44
|
+
]
|
|
45
|
+
}
|
|
46
|
+
]
|
|
47
|
+
}
|
|
48
|
+
]
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "feature-authentication",
|
|
3
|
+
"label": "Feature Authentication",
|
|
4
|
+
"description": "Authentication feature for Salesforce Web Applications",
|
|
5
|
+
"version": "1.0.0",
|
|
6
|
+
"outputDir": "dist",
|
|
7
|
+
"routing": {
|
|
8
|
+
"trailingSlash": "never",
|
|
9
|
+
"fallback": "/index.html"
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
|
|
4
|
+
"target": "ES2022",
|
|
5
|
+
"useDefineForClassFields": true,
|
|
6
|
+
"lib": ["ES2022", "DOM", "DOM.Iterable"],
|
|
7
|
+
"module": "ESNext",
|
|
8
|
+
"types": ["vite/client"],
|
|
9
|
+
"skipLibCheck": true,
|
|
10
|
+
|
|
11
|
+
/* Bundler mode */
|
|
12
|
+
"moduleResolution": "bundler",
|
|
13
|
+
"allowImportingTsExtensions": true,
|
|
14
|
+
"verbatimModuleSyntax": true,
|
|
15
|
+
"moduleDetection": "force",
|
|
16
|
+
"noEmit": true,
|
|
17
|
+
"jsx": "react-jsx",
|
|
18
|
+
|
|
19
|
+
/* Linting */
|
|
20
|
+
"strict": true,
|
|
21
|
+
"noUnusedLocals": true,
|
|
22
|
+
"noUnusedParameters": true,
|
|
23
|
+
"erasableSyntaxOnly": true,
|
|
24
|
+
"noFallthroughCasesInSwitch": true,
|
|
25
|
+
"noUncheckedSideEffectImports": true
|
|
26
|
+
},
|
|
27
|
+
"include": ["template/webApp"]
|
|
28
|
+
}
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
|
|
4
|
+
"target": "ES2023",
|
|
5
|
+
"lib": ["ES2023"],
|
|
6
|
+
"module": "ESNext",
|
|
7
|
+
"types": ["node"],
|
|
8
|
+
"skipLibCheck": true,
|
|
9
|
+
|
|
10
|
+
/* Bundler mode */
|
|
11
|
+
"moduleResolution": "bundler",
|
|
12
|
+
"allowImportingTsExtensions": true,
|
|
13
|
+
"verbatimModuleSyntax": true,
|
|
14
|
+
"moduleDetection": "force",
|
|
15
|
+
"noEmit": true,
|
|
16
|
+
|
|
17
|
+
/* Linting */
|
|
18
|
+
"strict": true,
|
|
19
|
+
"noUnusedLocals": true,
|
|
20
|
+
"noUnusedParameters": true,
|
|
21
|
+
"erasableSyntaxOnly": true,
|
|
22
|
+
"noFallthroughCasesInSwitch": true,
|
|
23
|
+
"noUncheckedSideEffectImports": true
|
|
24
|
+
},
|
|
25
|
+
"include": ["vite.config.ts"]
|
|
26
|
+
}
|