Skip to content

Commit 75e9e24

Browse files
authored
Merge pull request #3277 from code-asher/logout
2 parents b9ff73a + 8b2c78c commit 75e9e24

File tree

16 files changed

+128
-126
lines changed

16 files changed

+128
-126
lines changed

lib/vscode/.eslintignore

+1
Original file line numberDiff line numberDiff line change
@@ -19,3 +19,4 @@
1919
# These are code-server code symlinks.
2020
src/vs/base/node/proxy_agent.ts
2121
src/vs/ipc.d.ts
22+
src/vs/server/common/util.ts

lib/vscode/src/vs/server/browser/client.ts

+40-52
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
import * as path from 'vs/base/common/path';
2-
import { URI } from 'vs/base/common/uri';
32
import { Options } from 'vs/ipc';
43
import { localize } from 'vs/nls';
4+
import { MenuId, MenuRegistry } from 'vs/platform/actions/common/actions';
5+
import { CommandsRegistry } from 'vs/platform/commands/common/commands';
56
import { Extensions, IConfigurationRegistry } from 'vs/platform/configuration/common/configurationRegistry';
7+
import { ContextKeyExpr, IContextKeyService } from 'vs/platform/contextkey/common/contextkey';
68
import { registerSingleton } from 'vs/platform/instantiation/common/extensions';
79
import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection';
810
import { ILogService } from 'vs/platform/log/common/log';
@@ -11,10 +13,18 @@ import { Registry } from 'vs/platform/registry/common/platform';
1113
import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage';
1214
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
1315
import { TelemetryChannelClient } from 'vs/server/common/telemetry';
16+
import { getOptions } from 'vs/server/common/util';
1417
import 'vs/workbench/contrib/localizations/browser/localizations.contribution';
1518
import 'vs/workbench/services/localizations/browser/localizationsService';
1619
import { IRemoteAgentService } from 'vs/workbench/services/remote/common/remoteAgentService';
1720

21+
/**
22+
* All client-side customization to VS Code should live in this file when
23+
* possible.
24+
*/
25+
26+
const options = getOptions<Options>();
27+
1828
class TelemetryService extends TelemetryChannelClient {
1929
public constructor(
2030
@IRemoteAgentService remoteAgentService: IRemoteAgentService,
@@ -23,26 +33,6 @@ class TelemetryService extends TelemetryChannelClient {
2333
}
2434
}
2535

26-
/**
27-
* Remove extra slashes in a URL.
28-
*/
29-
export const normalize = (url: string, keepTrailing = false): string => {
30-
return url.replace(/\/\/+/g, '/').replace(/\/+$/, keepTrailing ? '/' : '');
31-
};
32-
33-
/**
34-
* Get options embedded in the HTML.
35-
*/
36-
export const getOptions = <T extends Options>(): T => {
37-
try {
38-
return JSON.parse(document.getElementById('coder-options')!.getAttribute('data-settings')!);
39-
} catch (error) {
40-
return {} as T;
41-
}
42-
};
43-
44-
const options = getOptions();
45-
4636
const TELEMETRY_SECTION_ID = 'telemetry';
4737
Registry.as<IConfigurationRegistry>(Extensions.Configuration).registerConfiguration({
4838
'id': TELEMETRY_SECTION_ID,
@@ -173,38 +163,36 @@ export const initialize = async (services: ServiceCollection): Promise<void> =>
173163
if (theme) {
174164
localStorage.setItem('colorThemeData', theme);
175165
}
176-
};
177166

178-
export interface Query {
179-
[key: string]: string | undefined;
180-
}
181-
182-
/**
183-
* Split a string up to the delimiter. If the delimiter doesn't exist the first
184-
* item will have all the text and the second item will be an empty string.
185-
*/
186-
export const split = (str: string, delimiter: string): [string, string] => {
187-
const index = str.indexOf(delimiter);
188-
return index !== -1 ? [str.substring(0, index).trim(), str.substring(index + 1)] : [str, ''];
189-
};
167+
// Use to show or hide logout commands and menu options.
168+
const contextKeyService = (services.get(IContextKeyService) as IContextKeyService);
169+
contextKeyService.createKey('code-server.authed', options.authed);
170+
171+
// Add a logout command.
172+
const logoutEndpoint = path.join(options.base, '/logout') + `?base=${options.base}`;
173+
const LOGOUT_COMMAND_ID = 'code-server.logout';
174+
CommandsRegistry.registerCommand(
175+
LOGOUT_COMMAND_ID,
176+
() => {
177+
window.location.href = logoutEndpoint;
178+
},
179+
);
180+
181+
// Add logout to command palette.
182+
MenuRegistry.appendMenuItem(MenuId.CommandPalette, {
183+
command: {
184+
id: LOGOUT_COMMAND_ID,
185+
title: localize('logout', "Log out")
186+
},
187+
when: ContextKeyExpr.has('code-server.authed')
188+
});
190189

191-
/**
192-
* Return the URL modified with the specified query variables. It's pretty
193-
* stupid so it probably doesn't cover any edge cases. Undefined values will
194-
* unset existing values. Doesn't allow duplicates.
195-
*/
196-
export const withQuery = (url: string, replace: Query): string => {
197-
const uri = URI.parse(url);
198-
const query = { ...replace };
199-
uri.query.split('&').forEach((kv) => {
200-
const [key, value] = split(kv, '=');
201-
if (!(key in query)) {
202-
query[key] = value;
203-
}
190+
// Add logout to the (web-only) home menu.
191+
MenuRegistry.appendMenuItem(MenuId.MenubarHomeMenu, {
192+
command: {
193+
id: LOGOUT_COMMAND_ID,
194+
title: localize('logout', "Log out")
195+
},
196+
when: ContextKeyExpr.has('code-server.authed')
204197
});
205-
return uri.with({
206-
query: Object.keys(query)
207-
.filter((k) => typeof query[k] !== 'undefined')
208-
.map((k) => `${k}=${query[k]}`).join('&'),
209-
}).toString(true);
210198
};

lib/vscode/src/vs/server/common/cookie.ts

-3
This file was deleted.
+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
../../../../../../src/common/util.ts

lib/vscode/src/vs/workbench/browser/parts/titlebar/menubarControl.ts

+2-27
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import { registerThemingParticipant, IThemeService } from 'vs/platform/theme/com
99
import { MenuBarVisibility, getTitleBarStyle, IWindowOpenable, getMenuBarVisibility } from 'vs/platform/windows/common/windows';
1010
import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey';
1111
import { IAction, Action, SubmenuAction, Separator } from 'vs/base/common/actions';
12-
import { addDisposableListener, Dimension, EventType, getCookieValue } from 'vs/base/browser/dom';
12+
import { addDisposableListener, Dimension, EventType } from 'vs/base/browser/dom';
1313
import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding';
1414
import { isMacintosh, isWeb, isIOS, isNative } from 'vs/base/common/platform';
1515
import { IConfigurationService, IConfigurationChangeEvent } from 'vs/platform/configuration/common/configuration';
@@ -38,8 +38,6 @@ import { KeyCode } from 'vs/base/common/keyCodes';
3838
import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry';
3939
import { IsWebContext } from 'vs/platform/contextkey/common/contextkeys';
4040
import { ICommandService } from 'vs/platform/commands/common/commands';
41-
import { ILogService } from 'vs/platform/log/common/log';
42-
import { Cookie } from 'vs/server/common/cookie';
4341

4442
export type IOpenRecentAction = IAction & { uri: URI, remoteAuthority?: string };
4543

@@ -318,8 +316,7 @@ export class CustomMenubarControl extends MenubarControl {
318316
@IThemeService private readonly themeService: IThemeService,
319317
@IWorkbenchLayoutService private readonly layoutService: IWorkbenchLayoutService,
320318
@IHostService protected readonly hostService: IHostService,
321-
@ICommandService commandService: ICommandService,
322-
@ILogService private readonly logService: ILogService
319+
@ICommandService commandService: ICommandService
323320
) {
324321
super(menuService, workspacesService, contextKeyService, keybindingService, configurationService, labelService, updateService, storageService, notificationService, preferencesService, environmentService, accessibilityService, hostService, commandService);
325322

@@ -721,28 +718,6 @@ export class CustomMenubarControl extends MenubarControl {
721718
webNavigationActions.pop();
722719
}
723720

724-
webNavigationActions.push(new Action('logout', localize('logout', "Log out"), undefined, true,
725-
async (event?: MouseEvent) => {
726-
const COOKIE_KEY = Cookie.Key;
727-
const loginCookie = getCookieValue(COOKIE_KEY);
728-
729-
this.logService.info('Logging out of code-server');
730-
731-
if(loginCookie) {
732-
this.logService.info(`Removing cookie under ${COOKIE_KEY}`);
733-
734-
if (document && document.cookie) {
735-
// We delete the cookie by setting the expiration to a date/time in the past
736-
document.cookie = COOKIE_KEY +'=; Path=/; Expires=Thu, 01 Jan 1970 00:00:01 GMT;';
737-
window.location.href = '/login';
738-
} else {
739-
this.logService.warn('Could not delete cookie because document and/or document.cookie is undefined');
740-
}
741-
} else {
742-
this.logService.warn('Could not log out because we could not find cookie');
743-
}
744-
}));
745-
746721
return webNavigationActions;
747722
}
748723

src/browser/register.ts

+6-3
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { logger } from "@coder/logger"
12
import { getOptions, normalize, logError } from "../common/util"
23

34
import "./pages/error.css"
@@ -6,19 +7,21 @@ import "./pages/login.css"
67

78
export async function registerServiceWorker(): Promise<void> {
89
const options = getOptions()
10+
logger.level = options.logLevel
11+
912
const path = normalize(`${options.csStaticBase}/dist/serviceWorker.js`)
1013
try {
1114
await navigator.serviceWorker.register(path, {
1215
scope: options.base + "/",
1316
})
14-
console.log("[Service Worker] registered")
17+
logger.info(`[Service Worker] registered`)
1518
} catch (error) {
16-
logError(`[Service Worker] registration`, error)
19+
logError(logger, `[Service Worker] registration`, error)
1720
}
1821
}
1922

2023
if (typeof navigator !== "undefined" && "serviceWorker" in navigator) {
2124
registerServiceWorker()
2225
} else {
23-
console.error(`[Service Worker] navigator is undefined`)
26+
logger.error(`[Service Worker] navigator is undefined`)
2427
}

src/common/util.ts

+14-6
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,13 @@
1-
import { logger, field } from "@coder/logger"
1+
/*
2+
* This file exists in two locations:
3+
* - src/common/util.ts
4+
* - lib/vscode/src/vs/server/common/util.ts
5+
* The second is a symlink to the first.
6+
*/
27

8+
/**
9+
* Base options included on every page.
10+
*/
311
export interface Options {
412
base: string
513
csStaticBase: string
@@ -69,6 +77,9 @@ export const getOptions = <T extends Options>(): T => {
6977
options = {} as T
7078
}
7179

80+
// You can also pass options in stringified form to the options query
81+
// variable. Options provided here will override the ones in the options
82+
// element.
7283
const params = new URLSearchParams(location.search)
7384
const queryOpts = params.get("options")
7485
if (queryOpts) {
@@ -78,13 +89,9 @@ export const getOptions = <T extends Options>(): T => {
7889
}
7990
}
8091

81-
logger.level = options.logLevel
82-
8392
options.base = resolveBase(options.base)
8493
options.csStaticBase = resolveBase(options.csStaticBase)
8594

86-
logger.debug("got options", field("options", options))
87-
8895
return options
8996
}
9097

@@ -113,7 +120,8 @@ export const getFirstString = (value: string | string[] | object | undefined): s
113120
return typeof value === "string" ? value : undefined
114121
}
115122

116-
export function logError(prefix: string, err: any): void {
123+
// TODO: Might make sense to add Error handling to the logger itself.
124+
export function logError(logger: { error: (msg: string) => void }, prefix: string, err: Error | string): void {
117125
if (err instanceof Error) {
118126
logger.error(`${prefix}: ${err.message} ${err.stack}`)
119127
} else {

src/node/app.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ export const createApp = async (args: DefaultedArgs): Promise<[Express, Express,
3737
reject(err)
3838
} else {
3939
// Promise resolved earlier so this is an unrelated error.
40-
util.logError("http server error", err)
40+
util.logError(logger, "http server error", err)
4141
}
4242
})
4343

src/node/routes/index.ts

+4-3
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import * as apps from "./apps"
2020
import * as domainProxy from "./domainProxy"
2121
import * as health from "./health"
2222
import * as login from "./login"
23+
import * as logout from "./logout"
2324
import * as pathProxy from "./pathProxy"
2425
// static is a reserved keyword.
2526
import * as _static from "./static"
@@ -136,10 +137,10 @@ export const register = async (
136137

137138
if (args.auth === AuthType.Password) {
138139
app.use("/login", login.router)
140+
app.use("/logout", logout.router)
139141
} else {
140-
app.all("/login", (req, res) => {
141-
redirect(req, res, "/", {})
142-
})
142+
app.all("/login", (req, res) => redirect(req, res, "/", {}))
143+
app.all("/logout", (req, res) => redirect(req, res, "/", {}))
143144
}
144145

145146
app.use("/static", _static.router)

src/node/routes/logout.ts

+17
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import { Router } from "express"
2+
import { getCookieDomain, redirect } from "../http"
3+
import { Cookie } from "./login"
4+
5+
export const router = Router()
6+
7+
router.get("/", async (req, res) => {
8+
// Must use the *identical* properties used to set the cookie.
9+
res.clearCookie(Cookie.Key, {
10+
domain: getCookieDomain(req.headers.host || "", req.args["proxy-domain"]),
11+
path: req.body.base || "/",
12+
sameSite: "lax",
13+
})
14+
15+
const to = (typeof req.query.to === "string" && req.query.to) || "/"
16+
return redirect(req, res, to, { to: undefined, base: undefined })
17+
})

src/node/routes/vscode.ts

+3-1
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { Request, Router } from "express"
33
import { promises as fs } from "fs"
44
import * as path from "path"
55
import qs from "qs"
6+
import * as ipc from "../../../typings/ipc"
67
import { Emitter } from "../../common/emitter"
78
import { HttpCode, HttpError } from "../../common/http"
89
import { getFirstString } from "../../common/util"
@@ -39,12 +40,13 @@ router.get("/", async (req, res) => {
3940
options.productConfiguration.codeServerVersion = version
4041

4142
res.send(
42-
replaceTemplates(
43+
replaceTemplates<ipc.Options>(
4344
req,
4445
// Uncomment prod blocks if not in development. TODO: Would this be
4546
// better as a build step? Or maintain two HTML files again?
4647
commit !== "development" ? content.replace(/<!-- PROD_ONLY/g, "").replace(/END_PROD_ONLY -->/g, "") : content,
4748
{
49+
authed: req.args.auth !== "none",
4850
disableTelemetry: !!req.args["disable-telemetry"],
4951
disableUpdateCheck: !!req.args["disable-update-check"],
5052
},

test/unit/register.test.ts

+9-8
Original file line numberDiff line numberDiff line change
@@ -22,11 +22,11 @@ describe("register", () => {
2222
})
2323

2424
beforeEach(() => {
25+
jest.clearAllMocks()
2526
jest.mock("@coder/logger", () => loggerModule)
2627
})
2728

2829
afterEach(() => {
29-
mockRegisterFn.mockClear()
3030
jest.resetModules()
3131
})
3232

@@ -39,6 +39,7 @@ describe("register", () => {
3939
global.navigator = (undefined as unknown) as Navigator & typeof globalThis
4040
global.location = (undefined as unknown) as Location & typeof globalThis
4141
})
42+
4243
it("test should have access to browser globals from beforeAll", () => {
4344
expect(typeof global.window).not.toBeFalsy()
4445
expect(typeof global.document).not.toBeFalsy()
@@ -74,24 +75,24 @@ describe("register", () => {
7475
})
7576

7677
describe("when navigator and serviceWorker are NOT defined", () => {
77-
let spy: jest.SpyInstance
78-
7978
beforeEach(() => {
80-
spy = jest.spyOn(console, "error")
79+
jest.clearAllMocks()
80+
jest.mock("@coder/logger", () => loggerModule)
8181
})
8282

8383
afterAll(() => {
8484
jest.restoreAllMocks()
8585
})
8686

87-
it("should log an error to the console", () => {
87+
it("should log an error", () => {
8888
// Load service worker like you would in the browser
8989
require("../../src/browser/register")
90-
expect(spy).toHaveBeenCalled()
91-
expect(spy).toHaveBeenCalledTimes(1)
92-
expect(spy).toHaveBeenCalledWith("[Service Worker] navigator is undefined")
90+
expect(loggerModule.logger.error).toHaveBeenCalled()
91+
expect(loggerModule.logger.error).toHaveBeenCalledTimes(1)
92+
expect(loggerModule.logger.error).toHaveBeenCalledWith("[Service Worker] navigator is undefined")
9393
})
9494
})
95+
9596
describe("registerServiceWorker", () => {
9697
let serviceWorkerPath: string
9798
let serviceWorkerScope: string

0 commit comments

Comments
 (0)