Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
1,009 changes: 606 additions & 403 deletions package-lock.json

Large diffs are not rendered by default.

11 changes: 5 additions & 6 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -1020,7 +1020,6 @@
"@cnblogs/prettier-config": "^2.0.3",
"@types/express": "^4.17.1",
"@types/glob": "^7.1.4",
"@types/lodash": "^4.14.178",
"@types/lodash-es": "^4.17.6",
"@types/markdown-it": "^12.2.3",
"@types/mime-types": "^2.1.1",
Expand Down Expand Up @@ -1059,6 +1058,7 @@
"ts-loader": "^9.2.5",
"tsconfig-paths-webpack-plugin": "^3.5.2",
"typescript": "^4.8.4",
"utility-types": "^3.10.0",
"webpack": "5.70.x",
"webpack-cli": "^4.8.0"
},
Expand All @@ -1074,19 +1074,18 @@
"download-chromium": "^2.2.1",
"express": "^4.17.1",
"form-data": "^4.0.0",
"got": "^12.5.3",
"got-fetch": "^5.1.4",
"is-wsl": "^2.2.0",
"lodash": "^4.17.21",
"lodash-es": "^4.17.21",
"markdown-it": "^12.3.2",
"markdown-it-table-of-contents": "^0.6.0",
"mime-types": "^2.1.34",
"node-fetch": "3.0.x",
"oidc-client": "^1.11.5",
"node-abort-controller": "^3.1.1",
"puppeteer-core": "^13.5.1",
"randomstring": "^1.1.4",
"react": "^17.0.2",
"react-dom": "^17.0.2",
"sanitize-filename": "^1.6.3",
"utility-types": "^3.10.0"
"sanitize-filename": "^1.6.3"
}
}
3 changes: 3 additions & 0 deletions src/authentication/access-token.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export interface AccessToken {
exp?: number;
}
61 changes: 61 additions & 0 deletions src/authentication/account-information.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import { UserInformationSpec } from '@/services/oauth.api';
import { trim } from 'lodash-es';
import { AuthenticationSessionAccountInformation } from 'vscode';
import { CnblogsAuthenticationProvider } from './authentication-provider';

export class CnblogsAccountInformation implements AuthenticationSessionAccountInformation {
readonly label: string;
readonly id: string;

private _blogApp?: string | null;

/**
* Creates an instance of {@link CnblogsAccountInformation}.
* @param {string} [name='unknown']
* @param {string} [avatar='']
* @param {string} [website=''] The user blog home page url
* @param {number} [blogId=-1]
* @param {string} [sub=''] UserId(data type is Guid)
* @param {number} [accountId=-1] SpaceUserId
*/
private constructor(
public readonly name: string,
public readonly avatar: string,
public readonly website: string,
public readonly blogId: number,
public readonly sub: string,
public readonly accountId: number
) {
this.id = `${this.accountId}-${CnblogsAuthenticationProvider.providerId}`;
this.label = name;
}

get userId() {
return this.sub;
}

get blogApp(): string | null {
if (this._blogApp == null) this._blogApp = this.parseBlogApp();

return this._blogApp;
}

static parse(userInfo: Partial<UserInformationSpec & CnblogsAccountInformation> = {}) {
return new CnblogsAccountInformation(
userInfo.name || 'anonymous',
userInfo.picture || userInfo.avatar || '',
userInfo.website || '',
userInfo.blog_id ? parseInt(userInfo.blog_id, 10) : userInfo.blogId ?? -1,
userInfo.sub || '',
userInfo.account_id ? parseInt(userInfo.account_id, 10) : userInfo.accountId ?? -1
);
}

private parseBlogApp() {
return (
trim(this.website ?? '', '/')
.split('/')
.pop() ?? null
);
}
}
131 changes: 131 additions & 0 deletions src/authentication/account-manager.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
import { CnblogsAccountInformation } from './account-information';
import { globalContext } from '../services/global-state';
import vscode, { authentication, AuthenticationGetSessionOptions, Disposable } from 'vscode';
import { accountViewDataProvider } from '../tree-view-providers/account-view-data-provider';
import { postsDataProvider } from '../tree-view-providers/posts-data-provider';
import { postCategoriesDataProvider } from '../tree-view-providers/post-categories-tree-data-provider';
import { OauthApi } from '@/services/oauth.api';
import { CnblogsAuthenticationProvider } from '@/authentication/authentication-provider';
import { CnblogsAuthenticationSession } from '@/authentication/session';

const isAuthorizedStorageKey = 'isAuthorized';

class AccountManager extends vscode.Disposable {
// eslint-disable-next-line @typescript-eslint/naming-convention
static readonly ACQUIRE_TOKEN_REJECT_UNAUTHENTICATED = 'unauthenticated';
// eslint-disable-next-line @typescript-eslint/naming-convention
static readonly ACQUIRE_TOKEN_REJECT_EXPIRED = 'expired';

private readonly _authenticationProvider: CnblogsAuthenticationProvider;
private readonly _disposable: vscode.Disposable;

private _oauthClient?: OauthApi | null;
private _session?: CnblogsAuthenticationSession | null;

constructor() {
super(() => {
this._disposable.dispose();
});

this._disposable = Disposable.from(
(this._authenticationProvider = CnblogsAuthenticationProvider.instance),
this._authenticationProvider.onDidChangeSessions(async ({ added }) => {
this._session = null;
if (added != null && added.length > 0) await this.ensureSession();

await this.updateAuthorizationStatus();

accountViewDataProvider.fireTreeDataChangedEvent();
postsDataProvider.fireTreeDataChangedEvent(undefined);
postCategoriesDataProvider.fireTreeDataChangedEvent();
})
);
}

get isAuthorized() {
return this._session != null;
}

get curUser(): CnblogsAccountInformation {
return this._session?.account ?? CnblogsAccountInformation.parse();
}

protected get oauthClient() {
return (this._oauthClient ??= new OauthApi());
}

/**
* Acquire the access token.
* This will reject with a human-readable reason string if not sign-in or the token has expired.
* @returns The access token of the active session
*/
async acquireToken(): Promise<string> {
const session = await this.ensureSession({ createIfNone: false });
return session == null
? Promise.reject(AccountManager.ACQUIRE_TOKEN_REJECT_UNAUTHENTICATED)
: session.hasExpired
? Promise.reject(AccountManager.ACQUIRE_TOKEN_REJECT_EXPIRED)
: session.accessToken;
}

async login() {
await this.ensureSession({ createIfNone: true, forceNewSession: false });
}

async logout() {
if (!this.isAuthorized) return;

const session = await authentication.getSession(CnblogsAuthenticationProvider.providerId, []);
if (session) await this._authenticationProvider.removeSession(session.id);

// For old version compatibility, **never** remove this line
await globalContext.storage.update('user', undefined);

if (session) {
return this.oauthClient
.revoke(session.accessToken)
.catch(console.warn)
.then(ok => (!ok ? console.warn('Revocation failed') : undefined));
}
}

setup() {
this.updateAuthorizationStatus().catch(console.warn);
}

private async updateAuthorizationStatus() {
await this.ensureSession({ createIfNone: false });
await vscode.commands.executeCommand(
'setContext',
`${globalContext.extensionName}.${isAuthorizedStorageKey}`,
this.isAuthorized
);
if (this.isAuthorized) {
await vscode.commands.executeCommand('setContext', `${globalContext.extensionName}.user`, {
name: this.curUser.name,
avatar: this.curUser.avatar,
});
}
}

private async ensureSession(
opt?: AuthenticationGetSessionOptions
): Promise<CnblogsAuthenticationSession | undefined | null> {
const session = await authentication.getSession(this._authenticationProvider.providerId, [], opt).then(
s => (s ? CnblogsAuthenticationSession.parse(s) : null),
() => null
);

if (session != null && session.account.accountId < 0) {
this._session = null;
await this._authenticationProvider.removeSession(session.id);
} else {
this._session = session;
}

return this._session ?? CnblogsAuthenticationSession.parse();
}
}

export const accountManager = new AccountManager();
export default accountManager;
Loading