Skip to content

Commit 114b96f

Browse files
committed
feat: ✨ use /api_auth endpoint for token retrieval
-took some inspiration from the official readwise plugin to use the `/api_auth` endpoint to retrieve the token. - improve the workflow in case the token becomes invalid (e.g. because of invalidation by the user) - validate token whenever it changes
1 parent 808548a commit 114b96f

File tree

3 files changed

+133
-21
lines changed

3 files changed

+133
-21
lines changed

src/main.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -785,7 +785,7 @@ export default class ReadwiseMirror extends Plugin {
785785
id: 'test',
786786
name: 'Test Readwise API key',
787787
callback: async () => {
788-
const isTokenValid = await this.readwiseApi.checkToken();
788+
const isTokenValid = await this.readwiseApi.hasValidToken();
789789
this.notify.notice('Readwise: ' + (isTokenValid ? 'Token is valid' : 'INVALID TOKEN'));
790790
},
791791
});

src/services/readwise-api.ts

+13-1
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ const API_PAGE_SIZE = 1000; // number of results per page, default 100 / max 100
77
export default class ReadwiseApi {
88
private apiToken: string;
99
private notify: Notify;
10+
private validToken: boolean = undefined;
1011

1112
constructor(apiToken: string, notify: Notify) {
1213
if (!apiToken) {
@@ -15,10 +16,14 @@ export default class ReadwiseApi {
1516

1617
this.setToken(apiToken);
1718
this.notify = notify;
19+
this.checkToken().then((isValid) => {
20+
this.validToken = isValid;
21+
});
1822
}
1923

2024
setToken(apiToken: string) {
2125
this.apiToken = apiToken;
26+
this.validToken = undefined;
2227
}
2328

2429
get headers() {
@@ -30,7 +35,14 @@ export default class ReadwiseApi {
3035
};
3136
}
3237

33-
async checkToken() {
38+
async hasValidToken() {
39+
if (this.validToken === undefined) {
40+
this.validToken = await this.checkToken();
41+
}
42+
return this.validToken;
43+
}
44+
45+
private async checkToken() {
3446
const results = await fetch(`${API_ENDPOINT}/auth`, this.headers);
3547

3648
return results.status === 204; // Returns a 204 response if token is valid

src/ui/settings-tab.ts

+119-19
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,53 @@ export default class ReadwiseMirrorSettingTab extends PluginSettingTab {
7777
}
7878
}
7979

80+
// Button-based authentication inspired by the official Readwise plugin
81+
private async getUserAuthToken(button: HTMLElement, attempt = 0): Promise<boolean> {
82+
const baseURL = 'https://readwise.io';
83+
const uuid = this.getReadwiseMirrorClientId();
84+
85+
if (attempt === 0) {
86+
window.open(`${baseURL}/api_auth?token=${uuid}&service=readwise-mirror`);
87+
}
88+
89+
let response, data;
90+
try {
91+
response = await fetch(`${baseURL}/api/auth?token=${uuid}`);
92+
if (response.ok) {
93+
data = await response.json();
94+
if (data.userAccessToken) {
95+
this.plugin.settings.apiToken = data.userAccessToken;
96+
this.plugin.readwiseApi.setToken(data.userAccessToken);
97+
await this.plugin.saveSettings();
98+
this.display(); // Refresh the settings page
99+
return true;
100+
}
101+
}
102+
} catch (e) {
103+
console.log("Failed to authenticate with Readwise:", e);
104+
}
105+
106+
if (attempt > 20) {
107+
this.notify.notice("Authentication timeout. Please try again.");
108+
return false;
109+
}
110+
111+
await new Promise(resolve => setTimeout(resolve, 1000));
112+
return this.getUserAuthToken(button, attempt + 1);
113+
}
114+
115+
// Button-based authentication inspired by the official Readwise plugin
116+
private getReadwiseMirrorClientId() {
117+
let readwiseMirrorClientId = window.localStorage.getItem('readwise-mirror-obsidian-client-id');
118+
if (readwiseMirrorClientId) {
119+
return readwiseMirrorClientId;
120+
} else {
121+
readwiseMirrorClientId = Math.random().toString(36).substring(2, 15);
122+
window.localStorage.setItem('readwise-mirror-obsidian-client-id', readwiseMirrorClientId);
123+
return readwiseMirrorClientId;
124+
}
125+
}
126+
80127
private createTemplateDocumentation(title: string, variables: [string, string][]) {
81128
return createFragment((fragment) => {
82129
const documentationContainer = fragment.createDiv({
@@ -118,29 +165,82 @@ export default class ReadwiseMirrorSettingTab extends PluginSettingTab {
118165

119166
containerEl.createEl('h1', { text: 'Readwise Sync Configuration' });
120167

121-
new Setting(containerEl)
122-
.setName('Authentication')
123-
.setHeading();
168+
// Authentication section inspired by the official Readwise plugin
169+
new Setting(containerEl).setName('Authentication').setHeading();
124170

125-
const apiTokenFragment = document.createDocumentFragment();
126-
apiTokenFragment.createEl('span', null, (spanEl) =>
127-
spanEl.createEl('a', null, (aEl) => (aEl.innerText = aEl.href = 'https://readwise.io/access_token'))
128-
);
171+
172+
const hasValidToken = await this.plugin.readwiseApi.hasValidToken();
173+
174+
const tokenValidationError = containerEl.createDiv({
175+
cls: 'setting-item-description validation-error',
176+
text: 'Invalid token. Please try authenticating again.',
177+
attr: {
178+
style: 'color: var(--text-error); margin-top: 0.5em; display: none;'
179+
}
180+
});;
181+
const tokenValidationSuccess = containerEl.createDiv({
182+
cls: 'setting-item-description validation-success',
183+
text: 'Token validated successfully',
184+
attr: {
185+
style: 'color: var(--text-success); margin-top: 0.5em; display: none;'
186+
}
187+
});
129188

130189
new Setting(containerEl)
131-
.setName('Enter your Readwise Access Token')
132-
.setDesc(apiTokenFragment)
133-
.addText((text) =>
190+
.setName('Readwise Authentication')
191+
.setDesc(createFragment((fragment) => {
192+
fragment.createEl('br');
193+
fragment.createEl('br');
194+
fragment.createEl('strong', { text: 'Important: ' });
195+
fragment.appendText('After successful authentication, a window with an error message will appear.');
196+
fragment.createEl('br');
197+
fragment.appendText('This is expected and can be safely closed.');
198+
fragment.createEl('br');
199+
fragment.createEl('br');
200+
fragment.append(tokenValidationError);
201+
fragment.append(tokenValidationSuccess);
202+
203+
// Show success or error message based on token validity
204+
if(hasValidToken) {
205+
tokenValidationSuccess.show();
206+
tokenValidationError.hide();
207+
} else {
208+
tokenValidationSuccess.hide();
209+
tokenValidationError.show();
210+
}
211+
}))
212+
.addButton((button) => {
213+
button
214+
.setButtonText(!hasValidToken ? 'Re-authenticate with Readwise' : 'Authenticate with Readwise')
215+
.setCta()
216+
.onClick(async (evt) => {
217+
const buttonEl = evt.target as HTMLElement;
218+
const token = await this.getUserAuthToken(buttonEl);
219+
if (token) {
220+
if (!hasValidToken) {
221+
tokenValidationError.show();
222+
tokenValidationSuccess.hide();
223+
} else {
224+
tokenValidationError.hide();
225+
tokenValidationSuccess.show();
226+
}
227+
}
228+
}).setDisabled(hasValidToken);
229+
return button;
230+
})
231+
.addText((text) => {
232+
const token = this.plugin.settings.apiToken;
233+
const maskedToken = token
234+
? token.slice(0, 6) + '*'.repeat(token.length - 6)
235+
: '';
236+
134237
text
135-
.setPlaceholder('Readwise Access Token')
136-
.setValue(this.plugin.settings.apiToken)
137-
.onChange(async (value) => {
138-
if (!value) return;
139-
this.plugin.settings.apiToken = value;
140-
await this.plugin.saveSettings();
141-
this.plugin.readwiseApi = new ReadwiseApi(value, this.notify);
142-
})
143-
);
238+
.setPlaceholder('Token will be filled automatically after authentication')
239+
.setValue(maskedToken)
240+
.setDisabled(true);
241+
});
242+
243+
144244

145245
new Setting(containerEl)
146246
.setName('Library Settings')

0 commit comments

Comments
 (0)