Skip to content

Commit 42e64a9

Browse files
fix: Fixed repeated auths prompt (close #652) (#799)
1 parent 73e0536 commit 42e64a9

File tree

4 files changed

+128
-3
lines changed

4 files changed

+128
-3
lines changed

src/common/types.ts

+4
Original file line numberDiff line numberDiff line change
@@ -288,6 +288,10 @@ export interface IAuth {
288288
password: string;
289289
}
290290

291+
export interface IStoredAuth {
292+
account: string;
293+
password: string;
294+
}
291295
export interface ISvnLogEntryPath {
292296
/** full path from repo root */
293297
_: string;

src/repository.ts

+58-3
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,8 @@ import {
2525
Status,
2626
SvnDepth,
2727
SvnUriAction,
28-
ISvnPathChange
28+
ISvnPathChange,
29+
IStoredAuth
2930
} from "./common/types";
3031
import { debounce, globalSequentialize, memoize, throttle } from "./decorators";
3132
import { exists } from "./fs";
@@ -50,6 +51,7 @@ import {
5051
} from "./util";
5152
import { match, matchAll } from "./util/globMatch";
5253
import { RepositoryFilesWatcher } from "./watchers/repositoryFilesWatcher";
54+
import { keytar } from "./vscodeModules";
5355

5456
function shouldShowProgress(operation: Operation): boolean {
5557
switch (operation) {
@@ -79,6 +81,7 @@ export class Repository implements IRemoteRepository {
7981
public needCleanUp: boolean = false;
8082
private remoteChangedUpdateInterval?: NodeJS.Timer;
8183
private deletedUris: Uri[] = [];
84+
private canSaveAuth: boolean = false;
8285

8386
private lastPromptAuth?: Thenable<IAuth | undefined>;
8487

@@ -919,6 +922,39 @@ export class Repository implements IRemoteRepository {
919922
return new PathNormalizer(this.repository.info);
920923
}
921924

925+
protected getCredentialServiceName() {
926+
let key = "vscode.svn-scm";
927+
928+
const info = this.repository.info;
929+
930+
if (info.repository && info.repository.root) {
931+
key += ":" + info.repository.root;
932+
} else if (info.url) {
933+
key += ":" + info.url;
934+
}
935+
936+
return key;
937+
}
938+
939+
public async loadStoredAuths(): Promise<Array<IStoredAuth>> {
940+
// Prevent multiple prompts for auth
941+
if (this.lastPromptAuth) {
942+
await this.lastPromptAuth;
943+
}
944+
return keytar.findCredentials(this.getCredentialServiceName());
945+
}
946+
947+
public async saveAuth(): Promise<void> {
948+
if (this.canSaveAuth && this.username && this.password) {
949+
await keytar.setPassword(
950+
this.getCredentialServiceName(),
951+
this.username,
952+
this.password
953+
);
954+
this.canSaveAuth = false;
955+
}
956+
}
957+
922958
public async promptAuth(): Promise<IAuth | undefined> {
923959
// Prevent multiple prompts for auth
924960
if (this.lastPromptAuth) {
@@ -931,6 +967,7 @@ export class Repository implements IRemoteRepository {
931967
if (result) {
932968
this.username = result.username;
933969
this.password = result.password;
970+
this.canSaveAuth = true;
934971
}
935972

936973
this.lastPromptAuth = undefined;
@@ -1002,11 +1039,14 @@ export class Repository implements IRemoteRepository {
10021039
runOperation: () => Promise<T> = () => Promise.resolve<any>(null)
10031040
): Promise<T> {
10041041
let attempt = 0;
1042+
let accounts: IStoredAuth[] = [];
10051043

10061044
while (true) {
10071045
try {
10081046
attempt++;
1009-
return await runOperation();
1047+
const result = await runOperation();
1048+
this.saveAuth();
1049+
return result;
10101050
} catch (err) {
10111051
if (
10121052
err.svnErrorCode === svnErrorCodes.RepositoryIsLocked &&
@@ -1016,7 +1056,22 @@ export class Repository implements IRemoteRepository {
10161056
await timeout(Math.pow(attempt, 2) * 50);
10171057
} else if (
10181058
err.svnErrorCode === svnErrorCodes.AuthorizationFailed &&
1019-
attempt <= 3
1059+
attempt <= 1 + accounts.length
1060+
) {
1061+
// First attempt load all stored auths
1062+
if (attempt === 1) {
1063+
accounts = await this.loadStoredAuths();
1064+
}
1065+
1066+
// each attempt, try a different account
1067+
const index = accounts.length - 1;
1068+
if (typeof accounts[index] !== "undefined") {
1069+
this.username = accounts[index].account;
1070+
this.password = accounts[index].password;
1071+
}
1072+
} else if (
1073+
err.svnErrorCode === svnErrorCodes.AuthorizationFailed &&
1074+
attempt <= 3 + accounts.length
10201075
) {
10211076
const result = await this.promptAuth();
10221077
if (!result) {

src/types/keytar.d.ts

+65
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
// Definitions by: Milan Burda <https://github.com/miniak>, Brendan Forster <https://github.com/shiftkey>, Hari Juturu <https://github.com/juturu>
2+
// Adapted from DefinitelyTyped: https://github.com/DefinitelyTyped/DefinitelyTyped/blob/master/types/keytar/index.d.ts
3+
4+
declare module "keytar" {
5+
/**
6+
* Get the stored password for the service and account.
7+
*
8+
* @param service The string service name.
9+
* @param account The string account name.
10+
*
11+
* @returns A promise for the password string.
12+
*/
13+
export declare function getPassword(
14+
service: string,
15+
account: string
16+
): Promise<string | null>;
17+
18+
/**
19+
* Add the password for the service and account to the keychain.
20+
*
21+
* @param service The string service name.
22+
* @param account The string account name.
23+
* @param password The string password.
24+
*
25+
* @returns A promise for the set password completion.
26+
*/
27+
export declare function setPassword(
28+
service: string,
29+
account: string,
30+
password: string
31+
): Promise<void>;
32+
33+
/**
34+
* Delete the stored password for the service and account.
35+
*
36+
* @param service The string service name.
37+
* @param account The string account name.
38+
*
39+
* @returns A promise for the deletion status. True on success.
40+
*/
41+
export declare function deletePassword(
42+
service: string,
43+
account: string
44+
): Promise<boolean>;
45+
46+
/**
47+
* Find a password for the service in the keychain.
48+
*
49+
* @param service The string service name.
50+
*
51+
* @returns A promise for the password string.
52+
*/
53+
export declare function findPassword(service: string): Promise<string | null>;
54+
55+
/**
56+
* Find all accounts and passwords for `service` in the keychain.
57+
*
58+
* @param service The string service name.
59+
*
60+
* @returns A promise for the array of found credentials.
61+
*/
62+
export declare function findCredentials(
63+
service: string
64+
): Promise<Array<{ account: string; password: string }>>;
65+
}

src/vscodeModules.ts

+1
Original file line numberDiff line numberDiff line change
@@ -30,3 +30,4 @@ export const iconv = loadVSCodeModule(
3030
export const jschardet = loadVSCodeModule(
3131
"jschardet"
3232
) as typeof import("jschardet");
33+
export const keytar = loadVSCodeModule("keytar") as typeof import("keytar");

0 commit comments

Comments
 (0)