1+ import { AuthConfig } from './types.js' ;
2+
3+ /**
4+ * Frontend authentication client for Stack Overflow Enterprise
5+ * This client communicates with your backend API to handle OAuth flows securely.
6+ * The backend performs all PKCE operations and token exchanges.
7+ */
8+ export class FrontendAuthClient {
9+ private config : AuthConfig ;
10+ private apiBaseUrl : string ;
11+
12+ /**
13+ * Creates a new frontend authentication client
14+ * @param config - Authentication configuration for Stack Overflow Enterprise
15+ * @param apiBaseUrl - Base URL of your backend API that handles OAuth operations
16+ */
17+ constructor ( config : AuthConfig , apiBaseUrl : string = '/api' ) {
18+ this . config = config ;
19+ this . apiBaseUrl = apiBaseUrl ;
20+ }
21+
22+ /**
23+ * Start the authentication process by requesting an authorization URL from your backend
24+ * Your backend will generate PKCE tokens and return the authorization URL
25+ *
26+ * @returns Promise resolving to the authorization URL where users should be redirected
27+ * @throws {Error } When the backend request fails
28+ * @example
29+ * ```typescript
30+ * const authUrl = await frontendClient.startAuth();
31+ * window.location.href = authUrl; // Redirect user to Stack Overflow Enterprise
32+ * ```
33+ */
34+ async startAuth ( ) : Promise < string > {
35+ try {
36+ const response = await this . requestAPI < { authUrl : string } > ( 'auth/start' ) ;
37+ return response . authUrl ;
38+ } catch ( error ) {
39+ throw new Error ( `Failed to start authentication: ${ error } ` ) ;
40+ }
41+ }
42+
43+ /**
44+ * Complete the authentication process by sending the callback parameters to your backend
45+ * Your backend will validate the state, exchange the code for tokens, and store them securely
46+ *
47+ * @param code - The authorization code received from Stack Overflow Enterprise callback
48+ * @param state - The state parameter received from Stack Overflow Enterprise callback
49+ * @throws {Error } When the backend request fails or authentication is invalid
50+ * @example
51+ * ```typescript
52+ * // In your callback page/component:
53+ * const urlParams = new URLSearchParams(window.location.search);
54+ * const code = urlParams.get('code');
55+ * const state = urlParams.get('state');
56+ *
57+ * if (code && state) {
58+ * await frontendClient.completeAuth(code, state);
59+ * // User is now authenticated, redirect to main app
60+ * }
61+ * ```
62+ */
63+ async completeAuth ( code : string , state : string ) : Promise < void > {
64+ try {
65+ await this . requestAPI ( 'callback' , 'GET' , undefined , [
66+ `code=${ encodeURIComponent ( code ) } ` ,
67+ `state=${ encodeURIComponent ( state ) } ` ,
68+ ] ) ;
69+ } catch ( error ) {
70+ throw new Error ( `Failed to complete authentication: ${ error } ` ) ;
71+ }
72+ }
73+
74+ /**
75+ * Check if the user is currently authenticated
76+ * Verifies with your backend whether valid authentication exists (e.g., via HTTP-only cookies)
77+ *
78+ * @returns Promise resolving to true if authenticated, false otherwise
79+ * @example
80+ * ```typescript
81+ * const isAuthenticated = await frontendClient.getAuthStatus();
82+ * if (!isAuthenticated) {
83+ * // Redirect to login
84+ * }
85+ * ```
86+ */
87+ async getAuthStatus ( ) : Promise < boolean > {
88+ try {
89+ await this . requestAPI ( 'authStatus' ) ;
90+ return true ;
91+ } catch {
92+ return false ;
93+ }
94+ }
95+
96+ /**
97+ * Log out the current user
98+ * Clears authentication state on your backend (e.g., removes HTTP-only cookies, clears tokens)
99+ *
100+ * @returns Promise resolving to true if logout was successful, false otherwise
101+ * @example
102+ * ```typescript
103+ * const loggedOut = await frontendClient.logout();
104+ * if (loggedOut) {
105+ * // Redirect to login page
106+ * }
107+ * ```
108+ */
109+ async logout ( ) : Promise < boolean > {
110+ try {
111+ await this . requestAPI ( 'logout' , 'POST' ) ;
112+ return true ;
113+ } catch {
114+ return false ;
115+ }
116+ }
117+
118+ /**
119+ * Submit an access token directly (for cases where user provides their own token)
120+ * Allows users to manually provide a Stack Overflow Enterprise access token
121+ *
122+ * @param token - The Stack Overflow Enterprise access token
123+ * @returns Promise resolving to true if token was accepted, false otherwise
124+ * @example
125+ * ```typescript
126+ * // If user has a personal access token:
127+ * const success = await frontendClient.submitAccessToken(userProvidedToken);
128+ * if (success) {
129+ * // Token stored successfully
130+ * }
131+ * ```
132+ */
133+ async submitAccessToken ( token : string ) : Promise < boolean > {
134+ try {
135+ await this . requestAPI ( 'auth/token' , 'POST' , { accessToken : token } ) ;
136+ return true ;
137+ } catch {
138+ return false ;
139+ }
140+ }
141+
142+ /**
143+ * Handle the OAuth callback in the current page
144+ * Parses the current URL for OAuth callback parameters and completes authentication
145+ *
146+ * @returns Promise resolving when authentication is complete
147+ * @throws {Error } When callback parameters are missing or authentication fails
148+ * @example
149+ * ```typescript
150+ * // In your OAuth callback page:
151+ * try {
152+ * await frontendClient.handleCallback();
153+ * // Success - user is authenticated
154+ * window.location.href = '/dashboard';
155+ * } catch (error) {
156+ * // Handle error
157+ * console.error('Authentication failed:', error);
158+ * }
159+ * ```
160+ */
161+ async handleCallback ( ) : Promise < void > {
162+ const url = new URL ( window . location . href ) ;
163+ const code = url . searchParams . get ( 'code' ) ;
164+ const state = url . searchParams . get ( 'state' ) ;
165+ const error = url . searchParams . get ( 'error' ) ;
166+
167+ if ( error ) {
168+ throw new Error ( `OAuth error: ${ error } ` ) ;
169+ }
170+
171+ if ( ! code || ! state ) {
172+ throw new Error ( 'Authorization code or state not found in callback URL' ) ;
173+ }
174+
175+ await this . completeAuth ( code , state ) ;
176+ }
177+
178+ /**
179+ * Make a request to your backend API
180+ * Internal utility method for communicating with your authentication backend
181+ *
182+ * @param endpoint - API endpoint path
183+ * @param method - HTTP method (defaults to 'GET')
184+ * @param body - Request body for POST/PUT requests
185+ * @param queryParams - Query parameters to append to the URL
186+ * @returns Promise resolving to the API response
187+ * @private
188+ */
189+ private async requestAPI < T = any > (
190+ endpoint : string ,
191+ method : string = 'GET' ,
192+ body ?: any ,
193+ queryParams ?: string [ ]
194+ ) : Promise < T > {
195+ const url = new URL ( `${ this . apiBaseUrl } /${ endpoint } ` , window . location . origin ) ;
196+
197+ if ( queryParams ) {
198+ url . search = queryParams . join ( '&' ) ;
199+ }
200+
201+ const options : RequestInit = {
202+ method,
203+ headers : {
204+ 'Content-Type' : 'application/json' ,
205+ } ,
206+ credentials : 'include' , // Important for HTTP-only cookies
207+ } ;
208+
209+ if ( body && ( method === 'POST' || method === 'PUT' ) ) {
210+ options . body = JSON . stringify ( body ) ;
211+ }
212+
213+ const response = await fetch ( url . toString ( ) , options ) ;
214+
215+ if ( ! response . ok ) {
216+ const errorText = await response . text ( ) ;
217+ throw new Error ( `API request failed: ${ response . status } ${ errorText } ` ) ;
218+ }
219+
220+ // Return empty object for successful requests with no content
221+ if ( response . status === 204 || response . headers . get ( 'content-length' ) === '0' ) {
222+ return { } as T ;
223+ }
224+
225+ return response . json ( ) ;
226+ }
227+ }
0 commit comments