Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

cody vs code: check whether user has verified email #51870

Merged
merged 9 commits into from
May 17, 2023
Merged
Show file tree
Hide file tree
Changes from 7 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
20 changes: 20 additions & 0 deletions client/cody-shared/src/sourcegraph-api/graphql/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
LEGACY_SEARCH_EMBEDDINGS_QUERY,
LOG_EVENT_MUTATION,
REPOSITORY_EMBEDDING_EXISTS_QUERY,
CURRENT_USER_ID_AND_VERIFIED_EMAIL,
mrnugget marked this conversation as resolved.
Show resolved Hide resolved
} from './queries'

interface APIResponse<T> {
Expand All @@ -24,6 +25,10 @@ interface CurrentUserIdResponse {
currentUser: { id: string } | null
}

interface CurrentUserIdAndVerifiedEmailResponse {
mrnugget marked this conversation as resolved.
Show resolved Hide resolved
currentUser: { id: string; hasVerifiedEmail: boolean } | null
}

interface RepositoryIdResponse {
repository: { id: string } | null
}
Expand Down Expand Up @@ -84,6 +89,10 @@ export class SourcegraphGraphQLAPIClient {
this.config = newConfig
}

public isDotCom(): boolean {
return new URL(this.config.serverEndpoint).origin === new URL(this.dotcomUrl).origin
}

public async getCurrentUserId(): Promise<string | Error> {
return this.fetchSourcegraphAPI<APIResponse<CurrentUserIdResponse>>(CURRENT_USER_ID_QUERY, {}).then(response =>
extractDataOrError(response, data =>
Expand All @@ -92,6 +101,17 @@ export class SourcegraphGraphQLAPIClient {
)
}

public async getCurrentUserIdAndVerifiedEmail(): Promise<{ id: string; hasVerifiedEmail: boolean } | Error> {
return this.fetchSourcegraphAPI<APIResponse<CurrentUserIdAndVerifiedEmailResponse>>(
CURRENT_USER_ID_AND_VERIFIED_EMAIL,
{}
).then(response =>
extractDataOrError(response, data =>
data.currentUser ? { ...data.currentUser } : new Error('current user not found')
)
)
}

public async getRepoId(repoName: string): Promise<string | Error> {
return this.fetchSourcegraphAPI<APIResponse<RepositoryIdResponse>>(REPOSITORY_ID_QUERY, {
name: repoName,
Expand Down
8 changes: 8 additions & 0 deletions client/cody-shared/src/sourcegraph-api/graphql/queries.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,14 @@ query CurrentUser {
}
}`

export const CURRENT_USER_ID_AND_VERIFIED_EMAIL = `
query CurrentUser {
currentUser {
id
hasVerifiedEmail
}
}`

export const REPOSITORY_ID_QUERY = `
query Repository($name: String!) {
repository(name: $name) {
Expand Down
1 change: 1 addition & 0 deletions client/cody/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ All notable changes to Sourcegraph Cody will be documented in this file.

- Removes unused configuration option: `cody.enabled`. [pull/51883](https://github.com/sourcegraph/sourcegraph/pull/51883)
- Arrow key behavior: you can now navigate forwards through messages with the down arrow; additionally the up and down arrows will navigate backwards and forwards only if you're at the start or end of the drafted text, respectively. [pull/51586](https://github.com/sourcegraph/sourcegraph/pull/51586)
- Display a more user-friendly error message when the user is connected to sourcegraph.com and doesn't have a verified email. [pull/51870](https://github.com/sourcegraph/sourcegraph/pull/51870)

## [0.1.2]

Expand Down
88 changes: 68 additions & 20 deletions client/cody/src/chat/ChatViewProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,14 +31,46 @@ import { LocalStorage } from '../services/LocalStorageProvider'
import { CODY_ACCESS_TOKEN_SECRET, SecretStorage } from '../services/SecretStorageProvider'
import { TestSupport } from '../test-support'

import { ConfigurationSubsetForWebview, DOTCOM_URL, ExtensionMessage, WebviewMessage } from './protocol'

export async function isValidLogin(
import {
AuthStatus,
ConfigurationSubsetForWebview,
DOTCOM_URL,
ExtensionMessage,
WebviewMessage,
isLoggedIn,
} from './protocol'

export async function getAuthStatus(
config: Pick<ConfigurationWithAccessToken, 'serverEndpoint' | 'accessToken' | 'customHeaders'>
): Promise<boolean> {
): Promise<AuthStatus> {
if (!config.accessToken) {
return {
showInvalidAccessTokenError: false,
authenticated: false,
hasVerifiedEmail: false,
requiresVerifiedEmail: false,
}
}

const client = new SourcegraphGraphQLAPIClient(config)
const userId = await client.getCurrentUserId()
return !isError(userId)
if (client.isDotCom()) {
const data = await client.getCurrentUserIdAndVerifiedEmail()
return {
showInvalidAccessTokenError: isError(data),
authenticated: !isError(data),
hasVerifiedEmail: !isError(data) && data?.hasVerifiedEmail,
// on sourcegraph.com this is always true
requiresVerifiedEmail: true,
}
}

const currentUserID = await client.getCurrentUserId()
return {
showInvalidAccessTokenError: isError(currentUserID),
authenticated: !isError(currentUserID),
hasVerifiedEmail: false,
requiresVerifiedEmail: false,
}
}

type Config = Pick<
Expand Down Expand Up @@ -187,19 +219,19 @@ export class ChatViewProvider implements vscode.WebviewViewProvider, vscode.Disp
await this.executeRecipe(message.recipe)
break
case 'settings': {
const isValid = await isValidLogin({
const authStatus = await getAuthStatus({
serverEndpoint: message.serverEndpoint,
accessToken: message.accessToken,
customHeaders: this.config.customHeaders,
})
// activate when user has valid login
await vscode.commands.executeCommand('setContext', 'cody.activated', isValid)
if (isValid) {
await vscode.commands.executeCommand('setContext', 'cody.activated', isLoggedIn(authStatus))
if (isLoggedIn(authStatus)) {
await updateConfiguration('serverEndpoint', message.serverEndpoint)
await this.secretStorage.store(CODY_ACCESS_TOKEN_SECRET, message.accessToken)
this.sendEvent('auth', 'login')
}
void this.webview?.postMessage({ type: 'login', isValid })
void this.webview?.postMessage({ type: 'login', authStatus })
break
}
case 'event':
Expand Down Expand Up @@ -289,7 +321,21 @@ export class ChatViewProvider implements vscode.WebviewViewProvider, vscode.Disp
this.transcript.addErrorAsAssistantResponse(err)
// Log users out on unauth error
if (statusCode && statusCode >= 400 && statusCode <= 410) {
void this.sendLogin(false)
if (statusCode === 403) {
void this.sendLogin({
showInvalidAccessTokenError: false,
authenticated: true,
hasVerifiedEmail: false,
requiresVerifiedEmail: true,
})
} else {
void this.sendLogin({
showInvalidAccessTokenError: true,
authenticated: false,
hasVerifiedEmail: false,
requiresVerifiedEmail: false,
})
}
void this.clearAndRestartSession()
}
this.onCompletionEnd()
Expand Down Expand Up @@ -509,13 +555,16 @@ export class ChatViewProvider implements vscode.WebviewViewProvider, vscode.Disp
/**
* Save Login state to webview
*/
public async sendLogin(isValid: boolean): Promise<void> {
public async sendLogin(authStatus: AuthStatus): Promise<void> {
this.sendEvent('token', 'Set')
await vscode.commands.executeCommand('setContext', 'cody.activated', isValid)
if (isValid) {
await vscode.commands.executeCommand('setContext', 'cody.activated', isLoggedIn(authStatus))
if (isLoggedIn(authStatus)) {
this.sendEvent('auth', 'login')
}
void this.webview?.postMessage({ type: 'login', isValid })
void this.webview?.postMessage({
type: 'login',
authStatus,
})
}

/**
Expand Down Expand Up @@ -585,14 +634,14 @@ export class ChatViewProvider implements vscode.WebviewViewProvider, vscode.Disp
void this.updateCodebaseContext()
// check if new configuration change is valid or not
// log user out if config is invalid
const isAuthed = await isValidLogin({
const authStatus = await getAuthStatus({
serverEndpoint: this.config.serverEndpoint,
accessToken: this.config.accessToken,
customHeaders: this.config.customHeaders,
})

// Ensure local app detector is running
if (!isAuthed) {
if (!isLoggedIn(authStatus)) {
this.localAppDetector.start()
} else {
this.localAppDetector.stop()
Expand All @@ -601,10 +650,9 @@ export class ChatViewProvider implements vscode.WebviewViewProvider, vscode.Disp
const configForWebview: ConfigurationSubsetForWebview = {
debug: this.config.debug,
serverEndpoint: this.config.serverEndpoint,
hasAccessToken: isAuthed,
}
void vscode.commands.executeCommand('setContext', 'cody.activated', isAuthed)
void this.webview?.postMessage({ type: 'config', config: configForWebview })
void vscode.commands.executeCommand('setContext', 'cody.activated', isLoggedIn(authStatus))
void this.webview?.postMessage({ type: 'config', config: configForWebview, authStatus })
}

this.disposables.push(this.configurationChangeEvent.event(() => send()))
Expand Down
23 changes: 18 additions & 5 deletions client/cody/src/chat/protocol.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,8 @@ export type WebviewMessage =
*/
export type ExtensionMessage =
| { type: 'showTab'; tab: string }
| { type: 'config'; config: ConfigurationSubsetForWebview }
| { type: 'login'; isValid: boolean }
| { type: 'config'; config: ConfigurationSubsetForWebview; authStatus: AuthStatus }
| { type: 'login'; authStatus: AuthStatus }
| { type: 'history'; messages: UserLocalHistory | null }
| { type: 'transcript'; messages: ChatMessage[]; isMessageInProgress: boolean }
| { type: 'debug'; message: string }
Expand All @@ -42,9 +42,22 @@ export type ExtensionMessage =
/**
* The subset of configuration that is visible to the webview.
*/
export interface ConfigurationSubsetForWebview extends Pick<Configuration, 'debug' | 'serverEndpoint'> {
hasAccessToken: boolean
}
export interface ConfigurationSubsetForWebview extends Pick<Configuration, 'debug' | 'serverEndpoint'> {}

export const DOTCOM_URL = new URL('https://sourcegraph.com')
export const LOCAL_APP_URL = new URL('http://localhost:3080')

/**
* The status of a users authentication, whether they're authenticated and have a
* verified email.
*/
export interface AuthStatus {
showInvalidAccessTokenError: boolean
authenticated: boolean
hasVerifiedEmail: boolean
requiresVerifiedEmail: boolean
}

export function isLoggedIn(authStatus: AuthStatus): boolean {
return authStatus.authenticated && (authStatus.requiresVerifiedEmail ? authStatus.hasVerifiedEmail : true)
}
14 changes: 7 additions & 7 deletions client/cody/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ import * as vscode from 'vscode'
import { RecipeID } from '@sourcegraph/cody-shared/src/chat/recipes/recipe'
import { ConfigurationWithAccessToken } from '@sourcegraph/cody-shared/src/configuration'

import { ChatViewProvider, isValidLogin } from './chat/ChatViewProvider'
import { DOTCOM_URL, LOCAL_APP_URL } from './chat/protocol'
import { ChatViewProvider, getAuthStatus } from './chat/ChatViewProvider'
import { DOTCOM_URL, LOCAL_APP_URL, isLoggedIn } from './chat/protocol'
import { CodyCompletionItemProvider } from './completions'
import { CompletionsDocumentProvider } from './completions/docprovider'
import { History } from './completions/history'
Expand Down Expand Up @@ -191,14 +191,14 @@ const register = async (
const token = params.get('code')
if (token && token.length > 8) {
await secretStorage.store(CODY_ACCESS_TOKEN_SECRET, token)
const isAuthed = await isValidLogin({
serverEndpoint,
const authStatus = await getAuthStatus({
serverEndpoint: DOTCOM_URL.href,
accessToken: token,
customHeaders: config.customHeaders,
})
await chatProvider.sendLogin(isAuthed)
if (isAuthed) {
void vscode.window.showInformationMessage('Token has been retreived and updated successfully')
await chatProvider.sendLogin(authStatus)
if (isLoggedIn(authStatus)) {
void vscode.window.showInformationMessage('Token has been retrieved and updated successfully')
}
}
},
Expand Down
7 changes: 6 additions & 1 deletion client/cody/webviews/App.story.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -44,9 +44,14 @@ const dummyVSCodeAPI: VSCodeWrapper = {
type: 'config',
config: {
debug: true,
hasAccessToken: true,
serverEndpoint: 'https://example.com',
},
authStatus: {
showInvalidAccessTokenError: false,
authenticated: true,
hasVerifiedEmail: false,
requiresVerifiedEmail: false,
},
})
return () => {}
},
Expand Down
Loading
Loading