basic dropbox auth flow
This commit is contained in:
parent
00c840a758
commit
1effcdd7f7
205
src/main.ts
205
src/main.ts
@ -28,7 +28,14 @@ import { isPasswordOk, getSyncPlan, doActualSync } from "./sync";
|
|||||||
|
|
||||||
import { S3Config, DEFAULT_S3_CONFIG } from "./s3";
|
import { S3Config, DEFAULT_S3_CONFIG } from "./s3";
|
||||||
import { WebdavConfig, DEFAULT_WEBDAV_CONFIG, WebdavAuthType } from "./webdav";
|
import { WebdavConfig, DEFAULT_WEBDAV_CONFIG, WebdavAuthType } from "./webdav";
|
||||||
import { DropboxConfig, DEFAULT_DROPBOX_CONFIG } from "./remoteForDropbox";
|
import {
|
||||||
|
DropboxConfig,
|
||||||
|
DEFAULT_DROPBOX_CONFIG,
|
||||||
|
getCodeVerifierAndChallenge,
|
||||||
|
getAuthUrl,
|
||||||
|
sendAuthReq,
|
||||||
|
setConfigBySuccessfullAuthInplace,
|
||||||
|
} from "./remoteForDropbox";
|
||||||
|
|
||||||
import { RemoteClient } from "./remote";
|
import { RemoteClient } from "./remote";
|
||||||
import { exportSyncPlansToFiles } from "./debugMode";
|
import { exportSyncPlansToFiles } from "./debugMode";
|
||||||
@ -185,7 +192,11 @@ export default class RemotelySavePlugin extends Plugin {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async loadSettings() {
|
async loadSettings() {
|
||||||
this.settings = Object.assign({}, DEFAULT_SETTINGS, await this.loadData());
|
this.settings = Object.assign(
|
||||||
|
{},
|
||||||
|
JSON.parse(JSON.stringify(DEFAULT_SETTINGS)) /* copy an object */,
|
||||||
|
await this.loadData()
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async saveSettings() {
|
async saveSettings() {
|
||||||
@ -259,6 +270,108 @@ export class PasswordModal extends Modal {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export class DropboxAuthModal extends Modal {
|
||||||
|
readonly plugin: RemotelySavePlugin;
|
||||||
|
readonly authDiv: HTMLDivElement;
|
||||||
|
readonly revokeAuthDiv: HTMLDivElement;
|
||||||
|
readonly revokeAuthSetting: Setting;
|
||||||
|
constructor(
|
||||||
|
app: App,
|
||||||
|
plugin: RemotelySavePlugin,
|
||||||
|
authDiv: HTMLDivElement,
|
||||||
|
revokeAuthDiv: HTMLDivElement,
|
||||||
|
revokeAuthSetting: Setting
|
||||||
|
) {
|
||||||
|
super(app);
|
||||||
|
this.plugin = plugin;
|
||||||
|
this.authDiv = authDiv;
|
||||||
|
this.revokeAuthDiv = revokeAuthDiv;
|
||||||
|
this.revokeAuthSetting = revokeAuthSetting;
|
||||||
|
}
|
||||||
|
|
||||||
|
onOpen() {
|
||||||
|
let { contentEl } = this;
|
||||||
|
|
||||||
|
const k = getCodeVerifierAndChallenge();
|
||||||
|
const authUrl = getAuthUrl(
|
||||||
|
this.plugin.settings.dropbox.clientID,
|
||||||
|
k.challenge
|
||||||
|
);
|
||||||
|
|
||||||
|
contentEl.createEl("p", {
|
||||||
|
text: "Step 1: Visit the following address in a browser, and follow the steps on the web page to authorize.",
|
||||||
|
});
|
||||||
|
contentEl.createEl("p").createEl("a", {
|
||||||
|
href: authUrl,
|
||||||
|
text: authUrl,
|
||||||
|
});
|
||||||
|
|
||||||
|
contentEl.createEl("p", {
|
||||||
|
text: 'Step 2: In the end of the web flow, you obtain a long code. Paste it here then click "Submit".',
|
||||||
|
});
|
||||||
|
|
||||||
|
let authCode = "";
|
||||||
|
new Setting(contentEl)
|
||||||
|
.setName("Auth Code from web page")
|
||||||
|
.setDesc('You need to click "Confirm".')
|
||||||
|
.addText((text) =>
|
||||||
|
text
|
||||||
|
.setPlaceholder("")
|
||||||
|
.setValue("")
|
||||||
|
.onChange((val) => {
|
||||||
|
authCode = val.trim();
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.addButton(async (button) => {
|
||||||
|
button.setButtonText("Confirm");
|
||||||
|
button.onClick(async () => {
|
||||||
|
new Notice("Trying to connect to Dropbox");
|
||||||
|
try {
|
||||||
|
const authRes = await sendAuthReq(
|
||||||
|
this.plugin.settings.dropbox.clientID,
|
||||||
|
k.verifier,
|
||||||
|
authCode
|
||||||
|
);
|
||||||
|
setConfigBySuccessfullAuthInplace(
|
||||||
|
this.plugin.settings.dropbox,
|
||||||
|
authRes
|
||||||
|
);
|
||||||
|
const client = new RemoteClient(
|
||||||
|
"dropbox",
|
||||||
|
undefined,
|
||||||
|
undefined,
|
||||||
|
this.plugin.settings.dropbox
|
||||||
|
);
|
||||||
|
const username = await client.getUser();
|
||||||
|
this.plugin.settings.dropbox.username = username;
|
||||||
|
await this.plugin.saveSettings();
|
||||||
|
new Notice(`Good! We've connected to Dropbox as user ${username}!`);
|
||||||
|
this.authDiv.toggleClass(
|
||||||
|
"dropbox-auth-button-hide",
|
||||||
|
this.plugin.settings.dropbox.username !== ""
|
||||||
|
);
|
||||||
|
this.revokeAuthDiv.toggleClass(
|
||||||
|
"dropbox-revoke-auth-button-hide",
|
||||||
|
this.plugin.settings.dropbox.username === ""
|
||||||
|
);
|
||||||
|
this.revokeAuthSetting.setDesc(
|
||||||
|
`You've connected as user ${this.plugin.settings.dropbox.username}. If you want to disconnect, click this button`
|
||||||
|
);
|
||||||
|
this.close();
|
||||||
|
} catch (err) {
|
||||||
|
console.log(err);
|
||||||
|
new Notice("Something goes wrong while connecting to Dropbox.");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
onClose() {
|
||||||
|
let { contentEl } = this;
|
||||||
|
contentEl.empty();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
class RemotelySaveSettingTab extends PluginSettingTab {
|
class RemotelySaveSettingTab extends PluginSettingTab {
|
||||||
plugin: RemotelySavePlugin;
|
plugin: RemotelySavePlugin;
|
||||||
|
|
||||||
@ -299,11 +412,11 @@ class RemotelySaveSettingTab extends PluginSettingTab {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// we need to create the div in advance of s3Div and webdavDiv
|
// we need to create the div in advance of any other service divs
|
||||||
const serviceChooserDiv = generalDiv.createEl("div");
|
const serviceChooserDiv = generalDiv.createEl("div");
|
||||||
|
|
||||||
let clickChooserTimes = 0;
|
let clickChooserTimes = 0;
|
||||||
serviceChooserDiv.onClickEvent((x) => {
|
serviceChooserDiv.onClickEvent(async (x) => {
|
||||||
if (Platform.isIosApp) {
|
if (Platform.isIosApp) {
|
||||||
// downgrade the experiment
|
// downgrade the experiment
|
||||||
// because iOS doesn't support x.detail
|
// because iOS doesn't support x.detail
|
||||||
@ -320,13 +433,13 @@ class RemotelySaveSettingTab extends PluginSettingTab {
|
|||||||
);
|
);
|
||||||
} else if (!this.plugin.settings.enableExperimentService) {
|
} else if (!this.plugin.settings.enableExperimentService) {
|
||||||
this.plugin.settings.enableExperimentService = true;
|
this.plugin.settings.enableExperimentService = true;
|
||||||
this.plugin.saveSettings();
|
await this.plugin.saveSettings();
|
||||||
new Notice(
|
new Notice(
|
||||||
"You've enabled hidden unstable experimental webdav support. Reopen settings again and try webdav with caution."
|
"You've enabled hidden unstable experimental webdav support. Reopen settings again and try webdav with caution."
|
||||||
);
|
);
|
||||||
} else if (this.plugin.settings.enableExperimentService) {
|
} else if (this.plugin.settings.enableExperimentService) {
|
||||||
this.plugin.settings.enableExperimentService = false;
|
this.plugin.settings.enableExperimentService = false;
|
||||||
this.plugin.saveSettings();
|
await this.plugin.saveSettings();
|
||||||
new Notice(
|
new Notice(
|
||||||
"You've disabled hidden unstable experimental webdav support. Reopen settings again."
|
"You've disabled hidden unstable experimental webdav support. Reopen settings again."
|
||||||
);
|
);
|
||||||
@ -474,21 +587,77 @@ class RemotelySaveSettingTab extends PluginSettingTab {
|
|||||||
cls: "dropbox-disclaimer",
|
cls: "dropbox-disclaimer",
|
||||||
});
|
});
|
||||||
dropboxDiv.createEl("p", {
|
dropboxDiv.createEl("p", {
|
||||||
text: "We create a folder App/obsidian-remotely-save on your Dropbox. All files/folders sync would happen inside this folder.",
|
text: "We will create a folder App/obsidian-remotely-save on your Dropbox. All files/folders sync would happen inside this folder.",
|
||||||
});
|
});
|
||||||
|
|
||||||
new Setting(dropboxDiv)
|
const dropboxSelectAuthDiv = dropboxDiv.createDiv();
|
||||||
.setName("access token")
|
const dropboxAuthDiv = dropboxSelectAuthDiv.createDiv({
|
||||||
.setDesc("access token")
|
cls: "dropbox-auth-button-hide",
|
||||||
.addText((text) =>
|
});
|
||||||
text
|
const dropboxRevokeAuthDiv = dropboxSelectAuthDiv.createDiv({
|
||||||
.setPlaceholder("")
|
cls: "dropbox-revoke-auth-button-hide",
|
||||||
.setValue(this.plugin.settings.dropbox.accessToken)
|
});
|
||||||
.onChange(async (value) => {
|
|
||||||
this.plugin.settings.dropbox.accessToken = value.trim();
|
const revokeAuthSetting = new Setting(dropboxRevokeAuthDiv)
|
||||||
|
.setName("Revoke Auth")
|
||||||
|
.setDesc(
|
||||||
|
`You've connected as user ${this.plugin.settings.dropbox.username}. If you want to disconnect, click this button`
|
||||||
|
)
|
||||||
|
.addButton(async (button) => {
|
||||||
|
button.setButtonText("Revoke Auth");
|
||||||
|
button.onClick(async () => {
|
||||||
|
try {
|
||||||
|
console.log("settings ");
|
||||||
|
console.log(this.plugin.settings.dropbox);
|
||||||
|
const client = new RemoteClient(
|
||||||
|
"dropbox",
|
||||||
|
undefined,
|
||||||
|
undefined,
|
||||||
|
this.plugin.settings.dropbox
|
||||||
|
);
|
||||||
|
await client.revokeAuth();
|
||||||
|
this.plugin.settings.dropbox = { ...DEFAULT_DROPBOX_CONFIG };
|
||||||
await this.plugin.saveSettings();
|
await this.plugin.saveSettings();
|
||||||
})
|
dropboxAuthDiv.toggleClass(
|
||||||
);
|
"dropbox-auth-button-hide",
|
||||||
|
this.plugin.settings.dropbox.username !== ""
|
||||||
|
);
|
||||||
|
dropboxRevokeAuthDiv.toggleClass(
|
||||||
|
"dropbox-revoke-auth-button-hide",
|
||||||
|
this.plugin.settings.dropbox.username === ""
|
||||||
|
);
|
||||||
|
new Notice("Revoked!");
|
||||||
|
} catch (err) {
|
||||||
|
console.log(err);
|
||||||
|
new Notice("Something goes wrong while revoking");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
new Setting(dropboxAuthDiv)
|
||||||
|
.setName("Auth")
|
||||||
|
.setDesc("Auth")
|
||||||
|
.addButton(async (button) => {
|
||||||
|
button.setButtonText("Auth");
|
||||||
|
button.onClick(async () => {
|
||||||
|
new DropboxAuthModal(
|
||||||
|
this.app,
|
||||||
|
this.plugin,
|
||||||
|
dropboxAuthDiv,
|
||||||
|
dropboxRevokeAuthDiv,
|
||||||
|
revokeAuthSetting
|
||||||
|
).open();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
dropboxAuthDiv.toggleClass(
|
||||||
|
"dropbox-auth-button-hide",
|
||||||
|
this.plugin.settings.dropbox.username !== ""
|
||||||
|
);
|
||||||
|
dropboxRevokeAuthDiv.toggleClass(
|
||||||
|
"dropbox-revoke-auth-button-hide",
|
||||||
|
this.plugin.settings.dropbox.username === ""
|
||||||
|
);
|
||||||
|
|
||||||
new Setting(dropboxDiv)
|
new Setting(dropboxDiv)
|
||||||
.setName("check connectivity")
|
.setName("check connectivity")
|
||||||
|
|||||||
@ -188,4 +188,20 @@ export class RemoteClient {
|
|||||||
throw Error(`not supported service type ${this.serviceType}`);
|
throw Error(`not supported service type ${this.serviceType}`);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
getUser = async () => {
|
||||||
|
if (this.serviceType === "dropbox") {
|
||||||
|
return await dropbox.getUserDisplayName(this.dropboxClient);
|
||||||
|
} else {
|
||||||
|
throw Error(`not supported service type ${this.serviceType}`);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
revokeAuth = async () => {
|
||||||
|
if (this.serviceType === "dropbox") {
|
||||||
|
return await dropbox.revokeAuth(this.dropboxClient);
|
||||||
|
} else {
|
||||||
|
throw Error(`not supported service type ${this.serviceType}`);
|
||||||
|
}
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,5 +1,7 @@
|
|||||||
import * as path from "path";
|
import * as path from "path";
|
||||||
import { FileStats, Vault } from "obsidian";
|
import { FileStats, Vault } from "obsidian";
|
||||||
|
import { Buffer } from "buffer";
|
||||||
|
import * as crypto from "crypto";
|
||||||
|
|
||||||
import { Dropbox, DropboxResponse, files } from "dropbox";
|
import { Dropbox, DropboxResponse, files } from "dropbox";
|
||||||
export { Dropbox } from "dropbox";
|
export { Dropbox } from "dropbox";
|
||||||
@ -17,10 +19,22 @@ import { strict as assert } from "assert";
|
|||||||
|
|
||||||
export interface DropboxConfig {
|
export interface DropboxConfig {
|
||||||
accessToken: string;
|
accessToken: string;
|
||||||
|
clientID: string;
|
||||||
|
refreshToken: string;
|
||||||
|
accessTokenExpiresInSeconds: number;
|
||||||
|
accessTokenExpiresAtTime: number;
|
||||||
|
accountID: string;
|
||||||
|
username: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const DEFAULT_DROPBOX_CONFIG = {
|
export const DEFAULT_DROPBOX_CONFIG = {
|
||||||
accessToken: "",
|
accessToken: "",
|
||||||
|
clientID: "",
|
||||||
|
refreshToken: "",
|
||||||
|
accessTokenExpiresInSeconds: 0,
|
||||||
|
accessTokenExpiresAtTime: 0,
|
||||||
|
accountID: "",
|
||||||
|
username: "",
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getDropboxPath = (fileOrFolderPath: string) => {
|
export const getDropboxPath = (fileOrFolderPath: string) => {
|
||||||
@ -370,3 +384,84 @@ export const checkConnectivity = async (dbx: Dropbox) => {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const getUserDisplayName = async (dbx: Dropbox) => {
|
||||||
|
const acct = await dbx.usersGetCurrentAccount();
|
||||||
|
return acct.result.name.display_name;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Dropbox authorization using PKCE
|
||||||
|
* see https://dropbox.tech/developers/pkce--what-and-why-
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
const specialBase64Encode = (str: Buffer) => {
|
||||||
|
return str
|
||||||
|
.toString("base64")
|
||||||
|
.replace(/\+/g, "-")
|
||||||
|
.replace(/\//g, "_")
|
||||||
|
.replace(/=/g, "");
|
||||||
|
};
|
||||||
|
const sha256 = (buffer: string) => {
|
||||||
|
return crypto.createHash("sha256").update(buffer).digest();
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getCodeVerifierAndChallenge = () => {
|
||||||
|
const codeVerifier = specialBase64Encode(crypto.randomBytes(32));
|
||||||
|
// console.log(`Client generated code_verifier: ${codeVerifier}`);
|
||||||
|
const codeChallenge = specialBase64Encode(sha256(codeVerifier));
|
||||||
|
// console.log(`Client generated code_challenge: ${codeChallenge}`);
|
||||||
|
return {
|
||||||
|
verifier: codeVerifier,
|
||||||
|
challenge: codeChallenge,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getAuthUrl = (appKey: string, challenge: string) => {
|
||||||
|
return `https://www.dropbox.com/oauth2/authorize?client_id=${appKey}&response_type=code&code_challenge=${challenge}&code_challenge_method=S256&token_access_type=offline`;
|
||||||
|
};
|
||||||
|
|
||||||
|
export interface DropboxSuccessAuthRes {
|
||||||
|
access_token: string;
|
||||||
|
token_type: string;
|
||||||
|
expires_in: number;
|
||||||
|
refresh_token: string;
|
||||||
|
scope: string;
|
||||||
|
uid: string;
|
||||||
|
account_id: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const sendAuthReq = async (
|
||||||
|
appKey: string,
|
||||||
|
verifier: string,
|
||||||
|
authCode: string
|
||||||
|
) => {
|
||||||
|
const resp1 = await fetch("https://api.dropboxapi.com/oauth2/token", {
|
||||||
|
method: "POST",
|
||||||
|
body: new URLSearchParams({
|
||||||
|
code: authCode,
|
||||||
|
grant_type: "authorization_code",
|
||||||
|
code_verifier: verifier,
|
||||||
|
client_id: appKey,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
const resp2 = (await resp1.json()) as DropboxSuccessAuthRes;
|
||||||
|
return resp2;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const setConfigBySuccessfullAuthInplace = (
|
||||||
|
config: DropboxConfig,
|
||||||
|
authRes: DropboxSuccessAuthRes
|
||||||
|
) => {
|
||||||
|
config.accessToken = authRes.access_token;
|
||||||
|
config.refreshToken = authRes.refresh_token;
|
||||||
|
config.accessTokenExpiresInSeconds = authRes.expires_in;
|
||||||
|
config.accessTokenExpiresAtTime =
|
||||||
|
Date.now() + authRes.expires_in * 1000 - 10 * 1000;
|
||||||
|
config.accountID = authRes.account_id;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const revokeAuth = async (client: Dropbox) => {
|
||||||
|
await client.authTokenRevoke();
|
||||||
|
};
|
||||||
|
|||||||
@ -18,6 +18,14 @@
|
|||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.dropbox-auth-button-hide {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropbox-revoke-auth-button-hide {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
.webdav-disclaimer {
|
.webdav-disclaimer {
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user