Skip to content

Commit

Permalink
cody vs code: check whether user has verified email (#51870)
Browse files Browse the repository at this point in the history
This fixes #51992 and
is a follow-up to #51706 to show a nicer message in VS Code in case user
hasn't confirmed their email yet.

It changes how we fetch the authentication status:
- if we're connected to dotcom, we check whether the user has a verified
email
- if we're not connected to dotcom, we skip that

If the user requires and doesn't have a verified email, we show a
different error on the Login page.

There rest of the changes are essentially there to handle config
changes, handle errors that happen at runtime, and login/logout.

**This needs a thorough review**

## Test plan

- See Loom: https://www.loom.com/share/f95a19cd9fd0419798fc888b49245ca0
- Manual testing
  • Loading branch information
mrnugget committed May 17, 2023
1 parent e632218 commit 8e27b90
Show file tree
Hide file tree
Showing 10 changed files with 228 additions and 76 deletions.
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_QUERY,
} from './queries'

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

interface CurrentUserIdHasVerifiedEmailResponse {
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<CurrentUserIdHasVerifiedEmailResponse>>(
CURRENT_USER_ID_AND_VERIFIED_EMAIL_QUERY,
{}
).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 = `
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

0 comments on commit 8e27b90

Please sign in to comment.