Skip to content

Commit 63b5c27

Browse files
committed
feat: integrate with vscode.authentication (#105)
1 parent d523a99 commit 63b5c27

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

66 files changed

+1580
-988
lines changed

package-lock.json

Lines changed: 606 additions & 403 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1020,7 +1020,6 @@
10201020
"@cnblogs/prettier-config": "^2.0.3",
10211021
"@types/express": "^4.17.1",
10221022
"@types/glob": "^7.1.4",
1023-
"@types/lodash": "^4.14.178",
10241023
"@types/lodash-es": "^4.17.6",
10251024
"@types/markdown-it": "^12.2.3",
10261025
"@types/mime-types": "^2.1.1",
@@ -1059,6 +1058,7 @@
10591058
"ts-loader": "^9.2.5",
10601059
"tsconfig-paths-webpack-plugin": "^3.5.2",
10611060
"typescript": "^4.8.4",
1061+
"utility-types": "^3.10.0",
10621062
"webpack": "5.70.x",
10631063
"webpack-cli": "^4.8.0"
10641064
},
@@ -1074,19 +1074,18 @@
10741074
"download-chromium": "^2.2.1",
10751075
"express": "^4.17.1",
10761076
"form-data": "^4.0.0",
1077+
"got": "^12.5.3",
1078+
"got-fetch": "^5.1.4",
10771079
"is-wsl": "^2.2.0",
1078-
"lodash": "^4.17.21",
10791080
"lodash-es": "^4.17.21",
10801081
"markdown-it": "^12.3.2",
10811082
"markdown-it-table-of-contents": "^0.6.0",
10821083
"mime-types": "^2.1.34",
1083-
"node-fetch": "3.0.x",
1084-
"oidc-client": "^1.11.5",
1084+
"node-abort-controller": "^3.1.1",
10851085
"puppeteer-core": "^13.5.1",
10861086
"randomstring": "^1.1.4",
10871087
"react": "^17.0.2",
10881088
"react-dom": "^17.0.2",
1089-
"sanitize-filename": "^1.6.3",
1090-
"utility-types": "^3.10.0"
1089+
"sanitize-filename": "^1.6.3"
10911090
}
10921091
}

src/authentication/access-token.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export interface AccessToken {
2+
exp?: number;
3+
}
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
import { UserInformationSpec } from '@/services/oauth.api';
2+
import { trim } from 'lodash-es';
3+
import { AuthenticationSessionAccountInformation } from 'vscode';
4+
import { CnblogsAuthenticationProvider } from './authentication-provider';
5+
6+
export class CnblogsAccountInformation implements AuthenticationSessionAccountInformation {
7+
readonly label: string;
8+
readonly id: string;
9+
10+
private _blogApp?: string | null;
11+
12+
/**
13+
* Creates an instance of {@link CnblogsAccountInformation}.
14+
* @param {string} [name='unknown']
15+
* @param {string} [avatar='']
16+
* @param {string} [website=''] The user blog home page url
17+
* @param {number} [blogId=-1]
18+
* @param {string} [sub=''] UserId(data type is Guid)
19+
* @param {number} [accountId=-1] SpaceUserId
20+
*/
21+
private constructor(
22+
public readonly name: string,
23+
public readonly avatar: string,
24+
public readonly website: string,
25+
public readonly blogId: number,
26+
public readonly sub: string,
27+
public readonly accountId: number
28+
) {
29+
this.id = `${this.accountId}-${CnblogsAuthenticationProvider.providerId}`;
30+
this.label = name;
31+
}
32+
33+
get userId() {
34+
return this.sub;
35+
}
36+
37+
get blogApp(): string | null {
38+
if (this._blogApp == null) this._blogApp = this.parseBlogApp();
39+
40+
return this._blogApp;
41+
}
42+
43+
static parse(userInfo: Partial<UserInformationSpec & CnblogsAccountInformation> = {}) {
44+
return new CnblogsAccountInformation(
45+
userInfo.name || 'anonymous',
46+
userInfo.picture || userInfo.avatar || '',
47+
userInfo.website || '',
48+
userInfo.blog_id ? parseInt(userInfo.blog_id, 10) : userInfo.blogId ?? -1,
49+
userInfo.sub || '',
50+
userInfo.account_id ? parseInt(userInfo.account_id, 10) : userInfo.accountId ?? -1
51+
);
52+
}
53+
54+
private parseBlogApp() {
55+
return (
56+
trim(this.website ?? '', '/')
57+
.split('/')
58+
.pop() ?? null
59+
);
60+
}
61+
}
Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
import { CnblogsAccountInformation } from './account-information';
2+
import { globalContext } from '../services/global-state';
3+
import vscode, { authentication, AuthenticationGetSessionOptions, Disposable } from 'vscode';
4+
import { accountViewDataProvider } from '../tree-view-providers/account-view-data-provider';
5+
import { postsDataProvider } from '../tree-view-providers/posts-data-provider';
6+
import { postCategoriesDataProvider } from '../tree-view-providers/post-categories-tree-data-provider';
7+
import { OauthApi } from '@/services/oauth.api';
8+
import { CnblogsAuthenticationProvider } from '@/authentication/authentication-provider';
9+
import { CnblogsAuthenticationSession } from '@/authentication/session';
10+
11+
const isAuthorizedStorageKey = 'isAuthorized';
12+
13+
class AccountManager extends vscode.Disposable {
14+
// eslint-disable-next-line @typescript-eslint/naming-convention
15+
static readonly ACQUIRE_TOKEN_REJECT_UNAUTHENTICATED = 'unauthenticated';
16+
// eslint-disable-next-line @typescript-eslint/naming-convention
17+
static readonly ACQUIRE_TOKEN_REJECT_EXPIRED = 'expired';
18+
19+
private readonly _authenticationProvider: CnblogsAuthenticationProvider;
20+
private readonly _disposable: vscode.Disposable;
21+
22+
private _oauthClient?: OauthApi | null;
23+
private _session?: CnblogsAuthenticationSession | null;
24+
25+
constructor() {
26+
super(() => {
27+
this._disposable.dispose();
28+
});
29+
30+
this._disposable = Disposable.from(
31+
(this._authenticationProvider = CnblogsAuthenticationProvider.instance),
32+
this._authenticationProvider.onDidChangeSessions(async ({ added }) => {
33+
this._session = null;
34+
if (added != null && added.length > 0) await this.ensureSession();
35+
36+
await this.updateAuthorizationStatus();
37+
38+
accountViewDataProvider.fireTreeDataChangedEvent();
39+
postsDataProvider.fireTreeDataChangedEvent(undefined);
40+
postCategoriesDataProvider.fireTreeDataChangedEvent();
41+
})
42+
);
43+
}
44+
45+
get isAuthorized() {
46+
return this._session != null;
47+
}
48+
49+
get curUser(): CnblogsAccountInformation {
50+
return this._session?.account ?? CnblogsAccountInformation.parse();
51+
}
52+
53+
protected get oauthClient() {
54+
return (this._oauthClient ??= new OauthApi());
55+
}
56+
57+
/**
58+
* Acquire the access token.
59+
* This will reject with a human-readable reason string if not sign-in or the token has expired.
60+
* @returns The access token of the active session
61+
*/
62+
async acquireToken(): Promise<string> {
63+
const session = await this.ensureSession({ createIfNone: false });
64+
return session == null
65+
? Promise.reject(AccountManager.ACQUIRE_TOKEN_REJECT_UNAUTHENTICATED)
66+
: session.hasExpired
67+
? Promise.reject(AccountManager.ACQUIRE_TOKEN_REJECT_EXPIRED)
68+
: session.accessToken;
69+
}
70+
71+
async login() {
72+
await this.ensureSession({ createIfNone: true, forceNewSession: false });
73+
}
74+
75+
async logout() {
76+
if (!this.isAuthorized) return;
77+
78+
const session = await authentication.getSession(CnblogsAuthenticationProvider.providerId, []);
79+
if (session) await this._authenticationProvider.removeSession(session.id);
80+
81+
// For old version compatibility, **never** remove this line
82+
await globalContext.storage.update('user', undefined);
83+
84+
if (session) {
85+
return this.oauthClient
86+
.revoke(session.accessToken)
87+
.catch(console.warn)
88+
.then(ok => (!ok ? console.warn('Revocation failed') : undefined));
89+
}
90+
}
91+
92+
setup() {
93+
this.updateAuthorizationStatus().catch(console.warn);
94+
}
95+
96+
private async updateAuthorizationStatus() {
97+
await this.ensureSession({ createIfNone: false });
98+
await vscode.commands.executeCommand(
99+
'setContext',
100+
`${globalContext.extensionName}.${isAuthorizedStorageKey}`,
101+
this.isAuthorized
102+
);
103+
if (this.isAuthorized) {
104+
await vscode.commands.executeCommand('setContext', `${globalContext.extensionName}.user`, {
105+
name: this.curUser.name,
106+
avatar: this.curUser.avatar,
107+
});
108+
}
109+
}
110+
111+
private async ensureSession(
112+
opt?: AuthenticationGetSessionOptions
113+
): Promise<CnblogsAuthenticationSession | undefined | null> {
114+
const session = await authentication.getSession(this._authenticationProvider.providerId, [], opt).then(
115+
s => (s ? CnblogsAuthenticationSession.parse(s) : null),
116+
() => null
117+
);
118+
119+
if (session != null && session.account.accountId < 0) {
120+
this._session = null;
121+
await this._authenticationProvider.removeSession(session.id);
122+
} else {
123+
this._session = session;
124+
}
125+
126+
return this._session ?? CnblogsAuthenticationSession.parse();
127+
}
128+
}
129+
130+
export const accountManager = new AccountManager();
131+
export default accountManager;

0 commit comments

Comments
 (0)