@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 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,9 @@
1
+ import { Outlet } from "react-router";
2
+
3
+ export default function AppLayout() {
4
+ return (
5
+ <>
6
+ <Outlet />
7
+ </>
8
+ );
9
+ }
@@ -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,7 @@
1
+ {
2
+ "files": [],
3
+ "references": [
4
+ { "path": "./tsconfig.app.json" },
5
+ { "path": "./tsconfig.node.json" }
6
+ ]
7
+ }
@@ -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
+ }