Skip to content
Draft
Show file tree
Hide file tree
Changes from 24 commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
6c2c335
Adding new controls
acrosman Jul 3, 2021
87f6e0d
Pass auth mode to main.
acrosman Jul 3, 2021
ce64da5
Refactor of login calls.
acrosman Jul 3, 2021
47f80cd
Initial draft of oauth interface (non-functional)
acrosman Jul 3, 2021
09337b4
Removed unused elements
acrosman Jul 3, 2021
8ecc59b
Fixed Login failure message
acrosman Jul 3, 2021
c90fb54
correction to partition setup
acrosman Jul 3, 2021
fa80e68
Further session correction
acrosman Jul 3, 2021
46b44e4
Revert "Further session correction"
acrosman Jul 3, 2021
25c34bf
partion name change
acrosman Jul 3, 2021
57bc59b
Merge branch 'main' into feature/add-oauth
acrosman Jan 16, 2022
ad80c65
Merge branch 'issue-54' into feature/add-oauth
acrosman Jan 16, 2022
95a2e32
minor tweaks
acrosman Mar 12, 2022
d434f52
Merge branch 'main' into feature/add-oauth
acrosman May 26, 2025
b1fc891
Finish Main back merge
acrosman May 26, 2025
a4f9304
Refactor password login into own function
acrosman May 26, 2025
4bcd394
Ensure passwords do not contain whitespace.
acrosman May 26, 2025
264060f
Switch to browser instead of local window
acrosman May 27, 2025
e12abee
Updates to use browser auth not local.
acrosman May 27, 2025
69d340d
And URL pattern filtering on open requests for better security.
acrosman May 27, 2025
28bb033
Remove extra Electron elements
acrosman May 27, 2025
ce3262e
Add schema check Login URL checker
acrosman May 27, 2025
853153a
Adding oauth test stub.
acrosman Oct 12, 2025
f4af7a9
Fix linting error
acrosman Oct 12, 2025
87792ed
Merge branch 'main' into feature/add-oauth
acrosman Apr 19, 2026
2f1922d
Merge branch 'main' into feature/add-oauth
acrosman Apr 19, 2026
f573c98
Remove unneeded tests
acrosman Apr 19, 2026
4df801b
Merge branch 'feature/add-oauth' of github.com:acrosman/Salesforce2Sq…
acrosman Apr 19, 2026
fa4cd9e
Adding oauth preferences and removing multiple connections
acrosman Apr 19, 2026
8d35f15
Update readme with OAuth instructions
acrosman Apr 19, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
SALESFORCE_CLIENT_ID=your_client_id_here
SALESFORCE_CLIENT_SECRET=your_client_secret_here
9 changes: 9 additions & 0 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@ jobs:
- name: install dependencies
run: npm install
- name: build
env:
SALESFORCE_CLIENT_ID: ${{ secrets.SALESFORCE_CLIENT_ID }}
SALESFORCE_CLIENT_SECRET: ${{ secrets.SALESFORCE_CLIENT_SECRET }}
run: npm run make

build_on_mac:
Expand All @@ -27,6 +30,9 @@ jobs:
- name: install dependencies
run: npm install
- name: build
env:
SALESFORCE_CLIENT_ID: ${{ secrets.SALESFORCE_CLIENT_ID }}
SALESFORCE_CLIENT_SECRET: ${{ secrets.SALESFORCE_CLIENT_SECRET }}
run: npm run make

build_on_win:
Expand All @@ -39,4 +45,7 @@ jobs:
- name: install dependencies
run: npm install
- name: build
env:
SALESFORCE_CLIENT_ID: ${{ secrets.SALESFORCE_CLIENT_ID }}
SALESFORCE_CLIENT_SECRET: ${{ secrets.SALESFORCE_CLIENT_SECRET }}
run: npm run make
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,8 @@ typings/

# dotenv environment variables file
.env
.env.local
.env.*.local

# parcel-bundler cache (https://parceljs.org/)
.cache
Expand Down
38 changes: 38 additions & 0 deletions ReadMe.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,3 +45,41 @@ This project has no direct association with Salesforce except the use of the API
## Getting Involved

If you would like to contribute to this project please feel invited to do so. Feel free to review open [issues](issues) and read the [contributing guide](contributing.md).

## Environment Setup

This application requires specific environment variables to be set for Salesforce OAuth authentication. Create a `.env` file in the root directory with the following variables:

```env
SALESFORCE_CLIENT_ID=your_client_id_here
SALESFORCE_CLIENT_SECRET=your_client_secret_here
```

### Getting the Credentials

1. Go to Salesforce Setup
2. Navigate to App Manager
3. Create a new Connected App or use an existing one
4. Configure OAuth settings:
- Enable OAuth Settings
- Callback URL: `http://localhost/completesetup`
- OAuth Scopes:
- Access and manage your data (api)
- Access your basic information (id)
- Perform requests on your behalf at any time (refresh_token)
5. After saving, copy the Consumer Key (Client ID) and Consumer Secret (Client Secret)
6. Add these values to your `.env` file

### Development Setup

1. Copy `.env.example` to `.env`:
```bash
cp .env.example .env
```
2. Edit `.env` with your Salesforce Connected App credentials
3. Run the application in development mode:
```bash
npm run dev
```

**Note:** Never commit the `.env` file to version control. The `.env.example` file is provided as a template.
57 changes: 39 additions & 18 deletions app/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -77,30 +77,51 @@ <h5 class="modal-title" id="loginModalLabel">Enter Salesforce Login</h5>
<div class="modal-body">
<form>
<div class="form-group">
<label for="login-username">User Name</label>
<input type="text" id="login-username" class="form-control"
aria-describedby="usernameHelp" placeholder="Enter Salesforce Username">
<small id="usernameHelp" class="form-text">Your Salesforce username must be
in the form of an email address.</small>
</div>
<div class="form-group">
<label for="login-password">Password</label>
<input type="password" id="login-password" class="form-control">
</div>
<div class="form-group">
<label for="login-token">Security Token</label>
<input type="password" class="form-control" aria-describedby="tokenHelp"
id="login-token">
<small id="tokenHelp" class="form-text">Likely you will need your Salesforce
Security token. If you do not have it follow <a
href='https://help.salesforce.com/articleView?id=user_security_token.htm&type=5'>these
reset instructions.</a></small>
<label for="sfconnect-radio-selectors">Select Connection Type</label>
<div class="form-check">
<input class="form-check-input" type="radio"
name="sfconnect-radio-selectors" id="sfconnect-oauth" value="oauth"
checked>
<label class="form-check-label" for="sfconnect-oauth">OAuth2</label>
</div>
<div class="form-check">
<input class="form-check-input" type="radio"
name="sfconnect-radio-selectors" id="sfconnect-password"
value="password">
<label class="form-check-label"
for="sfconnect-password">Username/Password</label>
</div>
</div>
<div class="form-group">
<label for="login-url">Login URL</label>
<input type="url" id="login-url" class="form-control"
value="https://login.salesforce.com">
</div>
<div id="login-password-wrapper">
<div class="form-group">
<label for="login-username">User Name</label>
<input type="text" id="login-username" class="form-control"
aria-describedby="usernameHelp"
placeholder="Enter Salesforce Username">
<small id="usernameHelp" class="form-text">Your Salesforce username must
be
in the form of an email address.</small>
</div>
<div class="form-group">
<label for="login-password">Password</label>
<input type="password" id="login-password" class="form-control">
</div>
<div class="form-group">
<label for="login-token">Security Token</label>
<input type="password" class="form-control" aria-describedby="tokenHelp"
id="login-token">
<small id="tokenHelp" class="form-text">Likely you will need your
Saleforce
Security token. If you do not have it follow <a
href='https://help.salesforce.com/articleView?id=user_security_token.htm&type=5'>these
reset instructions.</a></small>
</div>
</div>
</form>
</div>
<div class="modal-footer">
Expand Down
17 changes: 13 additions & 4 deletions app/render.js
Original file line number Diff line number Diff line change
Expand Up @@ -45,12 +45,19 @@ $.when($.ready).then(() => {
}
});

// Setup Object Select All
$('#btn-select-all-objects').on('click', (event) => {
event.preventDefault();
$('#results-table input[type=checkbox]').prop('checked', true);
// Setup login radio behaviors.
$('#login-password-wrapper').hide();
$('input[type=radio][name=sfconnect-radio-selectors]').on('change', (event) => {
if ($(event.target, ':checked').val() === 'oauth') {
$('#login-password-wrapper').hide();
} else {
$('#login-password-wrapper').show();
}
});

// Get the current application preferences.
window.api.send('get_preferences');

// Setup Object Select All
$('#btn-deselect-all-objects').on('click', (event) => {
event.preventDefault();
Expand Down Expand Up @@ -651,8 +658,10 @@ const updateSqlite3Path = (filePath) => {
// ========= Messages to the main process ===============
// Login
document.getElementById('login-trigger').addEventListener('click', () => {
const modeRadio = document.querySelector('input[type=radio][name="sfconnect-radio-selectors"]:checked');
showLoader('Attempting Login');
window.api.send('sf_login', {
mode: modeRadio.value,
username: document.getElementById('login-username').value,
password: document.getElementById('login-password').value,
token: document.getElementById('login-token').value,
Expand Down
3 changes: 2 additions & 1 deletion main.js
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ function createWindow() {
width: display.workArea.width,
height: display.workArea.height,
frame: true,
partiion: 'persist: secured-partition',
webPreferences: {
devTools: isDev,
nodeIntegration: false, // Disable nodeIntegration for security.
Expand Down Expand Up @@ -90,7 +91,7 @@ function createWindow() {
// https://www.electronjs.org/docs/tutorial/security#4-handle-session-permission-requests-from-remote-content
// https://github.com/doyensec/electronegativity/wiki/PERMISSION_REQUEST_HANDLER_GLOBAL_CHECK
session
.fromPartition('secured-partition')
.fromPartition('persist: secured-partition')
.setPermissionRequestHandler((webContents, permission, callback) => {
callback(false);
});
Expand Down
13 changes: 13 additions & 0 deletions src/config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
const config = {
oauth: {
clientId: process.env.SALESFORCE_CLIENT_ID || '',
clientSecret: process.env.SALESFORCE_CLIENT_SECRET || '',
scopes: ['api', 'id', 'web', 'refresh_token'],
},
updateOAuthCredentials(clientId, clientSecret) {
this.oauth.clientId = clientId;
this.oauth.clientSecret = clientSecret;
},
};

module.exports = config;
104 changes: 59 additions & 45 deletions src/sf_calls.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ const path = require('path');
const electron = require('electron');
const jsforce = require('jsforce');
const knex = require('knex');
const oauth = require('./sf_oauth2');
const constants = require('./constants');

// Get the dialog library from Electron
Expand Down Expand Up @@ -646,61 +647,74 @@ const buildDatabase = (settings) => {
});
};

const sfPasswordLogin = (url, username, password) => {
const conn = new jsforce.Connection({
loginUrl: url,
});

conn.login(username, password).then(
(userInfo) => {
// Now you can get the access token and instance URL information.
// Save them to establish connection next time.
logMessage('Password Login Attempt', 'Info', `Connection Org ${userInfo.organizationId} for User ${userInfo.id}`);

// Save the next connection in the global storage.
sfConnections[userInfo.organizationId] = {
instanceUrl: conn.instanceUrl,
accessToken: conn.accessToken,
version: '63.0',
};

mainWindow.webContents.send('response_login', {
status: true,
message: 'Login Successful',
response: userInfo,
limitInfo: conn.limitInfo,
request: {
username,
password: '********',
token: '********',
url,
},
});
},
(err) => {
mainWindow.webContents.send('response_login', {
status: false,
message: 'Login Failed',
response: err,
limitInfo: conn.limitInfo,
request: {
username,
password: '********',
token: '********',
url,
},
});
},
);
};

/**
* List of remote call handlers for using with IPC.
*/
const handlers = {
/**
* Login to an org using password authentication.
* Login to an org.
* @param {*} event Standard message event.
* @param {*} args Login credentials from the interface.
*/
sf_login: (event, args) => {
const conn = new jsforce.Connection({
loginUrl: args.url,
});

let { password } = args;
if (args.token !== '') {
password = `${password}${args.token}`;
if (args.mode === 'oauth') {
oauth.attemptLogin(args.url);
} else {
let { password } = args;
if (args.token && args.token.trim()) {
password = `${password.trim()}${args.token.trim()}`;
}
logMessage(event.sender.getTitle(), 'Info', 'Attempting Login with Basic Credentials');
sfPasswordLogin(args.url, args.username, password);
}

conn.login(args.username, password).then(
(userInfo) => {
// Since we send the args back to the interface, it's a good idea
// to remove the security information.
args.password = '';
args.token = '';

// Now you can get the access token and instance URL information.
// Save them to establish connection next time.
logMessage(event.sender.getTitle(), 'Info', `Connection Org ${userInfo.organizationId} for User ${userInfo.id}`);

// Save the next connection in the global storage.
sfConnections[userInfo.organizationId] = {
instanceUrl: conn.instanceUrl,
accessToken: conn.accessToken,
version: '63.0',
};

mainWindow.webContents.send('response_login', {
status: true,
message: 'Login Successful',
response: userInfo,
limitInfo: conn.limitInfo,
request: args,
});
},
(err) => {
mainWindow.webContents.send('response_login', {
status: false,
message: 'Login Failed',
response: err,
limitInfo: conn.limitInfo,
request: args,
});
},
);
},
/**
* Logout of a specific Salesforce org.
Expand Down
Loading
Loading