-
Notifications
You must be signed in to change notification settings - Fork 514
Feature: Netsuite oauth provider #891
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
sicarius97
wants to merge
14
commits into
hexclave:dev
Choose a base branch
from
sicarius97:feature/netsuite-oauth-provider
base: dev
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
14 commits
Select commit
Hold shift + click to select a range
e7a2288
feat: Add NetSuite OAuth 2.0 provider
sicarius97 0975c8a
Merge branch 'stack-auth:dev' into feature/netsuite-oauth-provider
sicarius97 ee776fc
Delete NETSUITE_OAUTH_IMPLEMENTATION.md
sicarius97 47025b5
Update NetSuite provider endpoints for OAuth
sicarius97 e43dc81
Rename accountId to netsuiteAccountId
sicarius97 3a526a4
Update apps/backend/src/oauth/providers/netsuite.tsx
sicarius97 755792e
Merge branch 'dev' into feature/netsuite-oauth-provider
sicarius97 2097be7
Merge branch 'dev' into feature/netsuite-oauth-provider
sicarius97 c9b2703
Merge remote-tracking branch 'origin/dev' into feature/netsuite-oauth…
BilalG1 81efa59
fix type
BilalG1 a78c8dc
added brand icon
BilalG1 c169493
type fixes
BilalG1 0d05ba4
fix test
BilalG1 0316084
Merge branch 'dev' into feature/netsuite-oauth-provider
BilalG1 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Some comments aren't visible on the classic Files Changed page.
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,128 @@ | ||
| import { getEnvVariable } from "@stackframe/stack-shared/dist/utils/env"; | ||
| import { StackAssertionError } from "@stackframe/stack-shared/dist/utils/errors"; | ||
| import { OAuthUserInfo, validateUserInfo } from "../utils"; | ||
| import { OAuthBaseProvider, TokenSet } from "./base"; | ||
|
|
||
| export class NetSuiteProvider extends OAuthBaseProvider { | ||
| private accountId: string; | ||
|
|
||
| private constructor( | ||
| accountId: string, | ||
| ...args: ConstructorParameters<typeof OAuthBaseProvider> | ||
| ) { | ||
| super(...args); | ||
| this.accountId = accountId; | ||
| } | ||
|
|
||
| static async create(options: { | ||
| clientId: string, | ||
| clientSecret: string, | ||
| netsuiteAccountId?: string, | ||
| }) { | ||
| const accountId = options.netsuiteAccountId || getEnvVariable("STACK_NETSUITE_ACCOUNT_ID", ""); | ||
| if (!accountId) { | ||
| throw new StackAssertionError("NetSuite Account ID is required. Set STACK_NETSUITE_ACCOUNT_ID environment variable or provide accountId in options."); | ||
| } | ||
|
|
||
| return new NetSuiteProvider( | ||
| accountId, | ||
| ...await OAuthBaseProvider.createConstructorArgs({ | ||
| issuer: `https://system.netsuite.com`, | ||
| authorizationEndpoint: `https://${accountId}.app.netsuite.com/app/login/oauth2/authorize.nl`, | ||
| tokenEndpoint: `https://${accountId}.suitetalk.api.netsuite.com/services/rest/auth/oauth2/v1/token`, | ||
| redirectUri: getEnvVariable("NEXT_PUBLIC_STACK_API_URL") + "/api/v1/auth/oauth/callback/netsuite", | ||
| baseScope: "rest_webservices", | ||
| tokenEndpointAuthMethod: "client_secret_basic", | ||
| // NetSuite access tokens typically expire in 1 hour | ||
| defaultAccessTokenExpiresInMillis: 1000 * 60 * 60, // 1 hour | ||
| ...options, | ||
| }) | ||
| ); | ||
| } | ||
|
|
||
| async postProcessUserInfo(tokenSet: TokenSet): Promise<OAuthUserInfo> { | ||
| // First, get the current user's employee record ID | ||
| const currentUserRes = await fetch(`https://${this.accountId}.suitetalk.api.netsuite.com/services/rest/auth/oauth2/v1/userinfo`, { | ||
| method: "GET", | ||
| headers: { | ||
| Authorization: `Bearer ${tokenSet.accessToken}`, | ||
| "Content-Type": "application/json", | ||
| "Accept": "application/json", | ||
| }, | ||
| }); | ||
|
|
||
| if (!currentUserRes.ok) { | ||
| // If employee endpoint fails, try to get basic user info from a different approach | ||
| // NetSuite doesn't have a standard userinfo endpoint, so we'll use what we can get | ||
| throw new StackAssertionError(`Error fetching user info from NetSuite: Status code ${currentUserRes.status}`, { | ||
| currentUserRes, | ||
| hasAccessToken: !!tokenSet.accessToken, | ||
| hasRefreshToken: !!tokenSet.refreshToken, | ||
| accessTokenExpiredAt: tokenSet.accessTokenExpiredAt, | ||
| }); | ||
| } | ||
|
|
||
| const userData = await currentUserRes.json(); | ||
|
|
||
| // NetSuite employee records structure can vary, but typically include: | ||
| // - id: internal ID | ||
| // - entityId: employee ID | ||
| // - firstName, lastName: name components | ||
| // - email: email address | ||
| let accountId: string; | ||
| let displayName: string | null = null; | ||
| let email: string | null = null; | ||
| let emailVerified = false; | ||
|
|
||
| if (userData.items && userData.items.length > 0) { | ||
| // If we get a list of employees, take the first one (current user) | ||
| const employee = userData.items[0]; | ||
| accountId = employee.id?.toString() || employee.entityId?.toString() || ""; | ||
| if (!accountId) { | ||
| throw new StackAssertionError("No valid ID found in NetSuite employee record", { employee }); | ||
| } | ||
| displayName = [employee.firstName, employee.lastName].filter(Boolean).join(" ") || employee.entityId; | ||
| email = employee.email; | ||
| emailVerified = !!employee.email; // Assume verified if present | ||
| } else if (userData.id) { | ||
| // If we get a single employee record | ||
| accountId = userData.id.toString(); | ||
| displayName = [userData.firstName, userData.lastName].filter(Boolean).join(" ") || userData.entityId; | ||
| email = userData.email; | ||
| emailVerified = !!userData.email; | ||
| } else { | ||
| throw new StackAssertionError("Unable to extract user information from NetSuite response", { | ||
| userData, | ||
| }); | ||
| } | ||
|
|
||
| if (!accountId) { | ||
| throw new StackAssertionError("No account ID found in NetSuite user data", { | ||
| userData, | ||
| }); | ||
| } | ||
|
|
||
| return validateUserInfo({ | ||
| accountId, | ||
| displayName, | ||
| email, | ||
| profileImageUrl: null, // NetSuite typically doesn't provide profile images via API | ||
| emailVerified, | ||
| }); | ||
| } | ||
|
|
||
| async checkAccessTokenValidity(accessToken: string): Promise<boolean> { | ||
| try { | ||
| const res = await fetch(`https://${this.accountId}.suitetalk.api.netsuite.com/services/rest/auth/oauth2/v1/userinfo`, { | ||
| method: "GET", | ||
| headers: { | ||
| Authorization: `Bearer ${accessToken}`, | ||
| "Content-Type": "application/json", | ||
| }, | ||
| }); | ||
| return res.ok; | ||
| } catch (error) { | ||
| return false; | ||
| } | ||
| } | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.