Skip to content

Commit 59ea6c1

Browse files
committed
PushsuP
1 parent 5a7e1c9 commit 59ea6c1

10 files changed

Lines changed: 186 additions & 17 deletions

File tree

src/app/preload/internalAPI.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,7 @@ import {
134134
ISSUES_API_REQUEST,
135135
PROXY_FETCH,
136136
SET_SERVER_AUTH_COOKIE,
137+
STORE_AUTO_LOGIN,
137138
AO_PICK_REPO_PATH,
138139
AO_SPAWN_SESSION,
139140
AO_SEND_MESSAGE,
@@ -180,6 +181,7 @@ contextBridge.exposeInMainWorld('desktop', {
180181
issuesApiRequest: (method, path, body) => ipcRenderer.invoke(ISSUES_API_REQUEST, method, path, body),
181182
proxyFetch: (url, options) => ipcRenderer.invoke(PROXY_FETCH, url, options),
182183
setServerAuthCookie: (serverUrl, token, userId) => ipcRenderer.invoke(SET_SERVER_AUTH_COOKIE, serverUrl, token, userId),
184+
storeAutoLogin: (serverUrl, email, password) => ipcRenderer.invoke(STORE_AUTO_LOGIN, serverUrl, email, password),
183185
closeTab: (viewId) => ipcRenderer.send(CLOSE_TAB, viewId),
184186
exitFullScreen: () => ipcRenderer.send(EXIT_FULLSCREEN),
185187
doubleClickOnWindow: () => ipcRenderer.send(DOUBLE_CLICK_ON_WINDOW),

src/app/views/OLIWebContentsView.ts

Lines changed: 47 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import {
2626
RELOAD_VIEW,
2727
} from 'common/communication';
2828
import type {Logger} from 'common/log';
29+
import {getAutoLogin, consumeAutoLogin} from 'common/autoLogin';
2930
import ServerManager from 'common/servers/serverManager';
3031
import {RELOAD_INTERVAL, MAX_SERVER_RETRIES, SECOND, MAX_LOADING_SCREEN_SECONDS} from 'common/utils/constants';
3132
import {isInternalURL, parseURL} from 'common/utils/url';
@@ -500,7 +501,7 @@ export class OLIWebContentsView extends EventEmitter {
500501
this.webContentsView.webContents.on('did-finish-load', () => {
501502
const url = this.webContentsView.webContents.getURL();
502503
if (url.includes('/login')) {
503-
this.injectCustomLoginUI();
504+
this.handleDidNavigate(url);
504505
}
505506
});
506507

@@ -537,10 +538,55 @@ export class OLIWebContentsView extends EventEmitter {
537538

538539
private handleDidNavigate = (url: string) => {
539540
if (url.includes('/login')) {
541+
// Check if we have auto-login credentials from the org setup flow
542+
const server = ServerManager.getServer(this.view.serverId);
543+
const serverUrl = server?.url?.toString()?.replace(/\/+$/, '');
544+
this.log.info(`[auto-login] Login page detected. serverUrl=${serverUrl}`);
545+
if (serverUrl) {
546+
const creds = getAutoLogin(serverUrl);
547+
this.log.info(`[auto-login] Credentials found: ${creds ? 'yes' : 'no'}`);
548+
if (creds) {
549+
this.autoLoginSilently(serverUrl, creds.email, creds.password);
550+
return;
551+
}
552+
}
540553
this.injectCustomLoginUI();
541554
}
542555
};
543556

557+
private autoLoginSilently = (serverUrl: string, email: string, password: string) => {
558+
const script = `
559+
(() => {
560+
// Hide all Mattermost UI immediately
561+
const rootEl = document.getElementById('root');
562+
if (rootEl) rootEl.style.display = 'none';
563+
document.body.style.background = '#0f1117';
564+
565+
async function doAutoLogin() {
566+
try {
567+
const loginRes = await fetch('/api/v4/users/login', {
568+
method: 'POST',
569+
headers: {'Content-Type': 'application/json'},
570+
body: JSON.stringify({ login_id: ${JSON.stringify(email)}, password: ${JSON.stringify(password)} }),
571+
});
572+
if (loginRes.ok) {
573+
// Login sets cookies automatically (same origin), reload to enter the app
574+
window.location.href = '/';
575+
} else {
576+
// If auto-login fails, show the root again so user can manually login
577+
if (rootEl) rootEl.style.display = '';
578+
}
579+
} catch {
580+
if (rootEl) rootEl.style.display = '';
581+
}
582+
}
583+
doAutoLogin();
584+
})();
585+
`;
586+
consumeAutoLogin(serverUrl);
587+
void this.webContentsView.webContents.executeJavaScript(script);
588+
};
589+
544590
private injectCustomLoginUI = () => {
545591
const server = ServerManager.getServer(this.view.serverId);
546592
const serverName = server?.name || 'your organization';

src/app/views/webContentEvents.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -307,6 +307,31 @@ export class WebContentsEventManager {
307307
const willNavigate = this.generateWillNavigate(contents.id);
308308
contents.on('will-navigate', willNavigate);
309309

310+
// Intercept Mattermost landing/login/signup pages — these should never be visible to the user
311+
const skipLandingPages = (event: Event, url: string) => {
312+
try {
313+
const parsed = new URL(url);
314+
const path = parsed.pathname.toLowerCase().replace(/\/+$/, '');
315+
if (path.endsWith('/landing') || path === '/signup_user_complete' || path === '/signup_email') {
316+
this.log(contents.id).info(`Intercepted Mattermost page: ${path}, redirecting to app`);
317+
event.preventDefault();
318+
319+
// Extract redirect_to param if present, otherwise go to root
320+
const redirectTo = parsed.searchParams.get('redirect_to');
321+
if (redirectTo && redirectTo.startsWith('/')) {
322+
contents.loadURL(`${parsed.origin}${redirectTo}`, {userAgent: composeUserAgent()});
323+
} else {
324+
// Go to /channels which auto-redirects to last team/channel with auth cookie
325+
contents.loadURL(`${parsed.origin}/channels`, {userAgent: composeUserAgent()});
326+
}
327+
}
328+
} catch {
329+
// ignore parse errors
330+
}
331+
};
332+
contents.on('will-redirect', skipLandingPages);
333+
contents.on('did-navigate', skipLandingPages);
334+
310335
const spellcheck = Config.useSpellChecker;
311336
const newWindow = this.generateNewWindowListener(contents.id, spellcheck);
312337
contents.setWindowOpenHandler(newWindow);
@@ -323,6 +348,8 @@ export class WebContentsEventManager {
323348
const removeWebContentsListeners = () => {
324349
try {
325350
contents.removeListener('will-navigate', willNavigate);
351+
contents.removeListener('will-redirect', skipLandingPages);
352+
contents.removeListener('did-navigate', skipLandingPages);
326353
contents.removeListener('console-message', consoleMessage);
327354
removeListeners?.(contents);
328355
} catch (e) {

src/common/autoLogin.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
// Copyright (c) 2016-present OLI, Inc. All Rights Reserved.
2+
// See LICENSE.txt for license information.
3+
4+
// Stores auto-login credentials from the org setup flow so the server view can
5+
// automatically log in without showing any Mattermost login pages.
6+
7+
const pendingLogins = new Map<string, {email: string; password: string}>();
8+
9+
export function storeAutoLogin(serverUrl: string, email: string, password: string) {
10+
// Normalize URL
11+
const normalized = serverUrl.replace(/\/+$/, '');
12+
pendingLogins.set(normalized, {email, password});
13+
}
14+
15+
export function consumeAutoLogin(serverUrl: string): {email: string; password: string} | undefined {
16+
const normalized = serverUrl.replace(/\/+$/, '');
17+
const creds = pendingLogins.get(normalized);
18+
if (creds) {
19+
pendingLogins.delete(normalized);
20+
}
21+
return creds;
22+
}
23+
24+
export function getAutoLogin(serverUrl: string): {email: string; password: string} | undefined {
25+
const normalized = serverUrl.replace(/\/+$/, '');
26+
return pendingLogins.get(normalized);
27+
}

src/common/communication.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,7 @@ export const ISSUES_API_REQUEST = 'issues-api-request';
104104
export const NAVIGATE_TO_ISSUE = 'navigate-to-issue';
105105
export const PROXY_FETCH = 'proxy-fetch';
106106
export const SET_SERVER_AUTH_COOKIE = 'set-server-auth-cookie';
107+
export const STORE_AUTO_LOGIN = 'store-auto-login';
107108

108109
export const UPDATE_PATHS = 'update-paths';
109110

src/main/app/initialize.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ import {
4343
NAVIGATE_TO_ISSUE,
4444
PROXY_FETCH,
4545
SET_SERVER_AUTH_COOKIE,
46+
STORE_AUTO_LOGIN,
4647
SET_VIEW_MODE,
4748
AO_PICK_REPO_PATH,
4849
AO_SPAWN_SESSION,
@@ -58,6 +59,7 @@ import {
5859
import Config from 'common/config';
5960
import buildConfig from 'common/config/buildConfig';
6061
import {MATTERMOST_PROTOCOL} from 'common/constants';
62+
import {storeAutoLogin} from 'common/autoLogin';
6163
import {Logger} from 'common/log';
6264
import ServerManager from 'common/servers/serverManager';
6365
import {parseURL} from 'common/utils/url';
@@ -441,6 +443,11 @@ function initializeInterCommunicationEventListeners() {
441443
});
442444
});
443445

446+
ipcMain.handle(STORE_AUTO_LOGIN, (_event, serverUrl: string, email: string, password: string) => {
447+
log.info(`[auto-login] Storing credentials for ${serverUrl}, email: ${email}`);
448+
storeAutoLogin(serverUrl, email, password);
449+
});
450+
444451
ipcMain.on(NAVIGATE_TO_ISSUE, (_event, issueId: string) => {
445452
log.info(`NAVIGATE_TO_ISSUE received in initialize.ts, issueId: ${issueId}`);
446453
TabManager.setViewMode('issues');

src/main/app/intercom.ts

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -108,12 +108,13 @@ export function handleWelcomeScreenModal(prefillURL?: string) {
108108
const modalPromise = ModalManager.addModal<{prefillURL?: string}, UniqueServer>(ModalConstants.WELCOME_SCREEN_MODAL, html, preload, {prefillURL}, mainWindow, !ServerManager.hasServers());
109109
if (modalPromise) {
110110
modalPromise.then(async (data) => {
111+
const parsedServerURL = parseURL(data.url);
111112
let initialLoadURL;
112-
if (prefillURL) {
113-
const parsedServerURL = parseURL(data.url);
114-
if (parsedServerURL) {
115-
initialLoadURL = parseURL(`${parsedServerURL.origin}${prefillURL.substring(prefillURL.indexOf('/'))}`);
116-
}
113+
114+
if (data.initialPath && parsedServerURL) {
115+
initialLoadURL = parseURL(`${parsedServerURL.origin}${data.initialPath}`);
116+
} else if (prefillURL && parsedServerURL) {
117+
initialLoadURL = parseURL(`${parsedServerURL.origin}${prefillURL.substring(prefillURL.indexOf('/'))}`);
117118
}
118119

119120
// Replace existing server if one exists (single-org model)

src/main/utils.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@ export function composeUserAgent(browserMode?: boolean) {
7575
return filteredUserAgent.join(' ');
7676
}
7777

78-
return `${filteredUserAgent.join(' ')} OLI/${app.getVersion()}`;
78+
return `${filteredUserAgent.join(' ')} Mattermost/${app.getVersion()} OLI/${app.getVersion()}`;
7979
}
8080

8181
export function isStringWithLength(string: unknown): boolean {

src/renderer/components/OrganizationList/OrganizationList.tsx

Lines changed: 67 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -276,11 +276,14 @@ function OrganizationList({provisioningApiUrl, onConnect}: OrganizationListProps
276276
throw new Error('Invalid login response');
277277
}
278278

279+
console.log('[org-setup] login response headers:', JSON.stringify(loginRes.headers));
280+
console.log('[org-setup] token extracted:', token ? `${token.substring(0, 8)}...` : 'NONE');
281+
279282
if (!token) {
280283
throw new Error('Login succeeded but no auth token received');
281284
}
282285

283-
// 3. Create team (or get existing)
286+
// 3. Create team or join existing one
284287
const teamName = sanitizeTeamName(resolvedOrg.name);
285288
let teamId: string;
286289

@@ -301,19 +304,38 @@ function OrganizationList({provisioningApiUrl, onConnect}: OrganizationListProps
301304
const team = JSON.parse(createTeamRes.body);
302305
teamId = team.id;
303306
} else {
304-
// Team may already exist (409) or user lacks permission to create (403) — try to join existing
305-
const getTeamRes = await proxyFetch(`${serverUrl}/api/v4/teams/name/${teamName}`, {
307+
// Team already exists — get all teams user can join and find it
308+
const allTeamsRes = await proxyFetch(`${serverUrl}/api/v4/teams?page=0&per_page=200`, {
306309
headers: {'Authorization': `Bearer ${token}`},
307310
});
308-
if (!getTeamRes.ok) {
309-
throw new Error('Failed to find existing team');
311+
312+
if (!allTeamsRes.ok) {
313+
throw new Error('Failed to list teams');
314+
}
315+
316+
const allTeams = JSON.parse(allTeamsRes.body);
317+
const match = allTeams.find((t: {name: string}) => t.name === teamName);
318+
319+
if (match) {
320+
teamId = match.id;
321+
} else {
322+
// Last resort: extract team ID from the detailed_error in the create response
323+
try {
324+
const err = JSON.parse(createTeamRes.body);
325+
const idMatch = err.detailed_error?.match(/id=([a-z0-9]+)/);
326+
if (idMatch) {
327+
teamId = idMatch[1];
328+
} else {
329+
throw new Error('Could not find team');
330+
}
331+
} catch {
332+
throw new Error('Team exists but could not be found. Ask an admin to invite you.');
333+
}
310334
}
311-
const team = JSON.parse(getTeamRes.body);
312-
teamId = team.id;
313335
}
314336

315337
// 4. Join team
316-
await proxyFetch(`${serverUrl}/api/v4/teams/${teamId}/members`, {
338+
const joinRes = await proxyFetch(`${serverUrl}/api/v4/teams/${teamId}/members`, {
317339
method: 'POST',
318340
headers: {
319341
'Content-Type': 'application/json',
@@ -325,11 +347,46 @@ function OrganizationList({provisioningApiUrl, onConnect}: OrganizationListProps
325347
}),
326348
});
327349

350+
console.log('[org-setup] join team response:', joinRes.status);
351+
328352
// 5. Set auth cookie so the Mattermost web app loads already logged in
329353
await window.desktop.setServerAuthCookie(serverUrl, token, user.id);
330354

331-
// 6. Done
332-
onConnect({url: serverUrl, name: resolvedOrg.name});
355+
// 6. Skip all Mattermost onboarding/landing pages via preferences API
356+
const authHeaders = {
357+
'Content-Type': 'application/json',
358+
'Authorization': `Bearer ${token}`,
359+
};
360+
361+
// Mark tutorial as completed, skip "tips", skip landing page
362+
await proxyFetch(`${serverUrl}/api/v4/users/${user.id}/preferences`, {
363+
method: 'PUT',
364+
headers: authHeaders,
365+
body: JSON.stringify([
366+
{user_id: user.id, category: 'tutorial_step', name: user.id, value: '999'},
367+
{user_id: user.id, category: 'insights', name: 'insights_tutorial_state', value: '{"insights_modal_viewed":true}'},
368+
{user_id: user.id, category: 'recommended_next_steps', name: 'hide', value: 'true'},
369+
{user_id: user.id, category: 'drafts', name: 'drafts_tour_tip_showed', value: '{"drafts_tour_tip_showed":true}'},
370+
{user_id: user.id, category: 'crt_thread_pane_step', name: user.id, value: '999'},
371+
]),
372+
});
373+
374+
// Also complete onboarding via the dedicated endpoint
375+
await proxyFetch(`${serverUrl}/api/v4/users/${user.id}/preferences`, {
376+
method: 'PUT',
377+
headers: authHeaders,
378+
body: JSON.stringify([
379+
{user_id: user.id, category: 'system_notice', name: 'GMasDM', value: 'true'},
380+
{user_id: user.id, category: 'onboarding_task_list', name: 'onboarding_task_list_show', value: 'false'},
381+
{user_id: user.id, category: 'onboarding_task_list', name: 'onboarding_task_list_open', value: 'false'},
382+
]),
383+
});
384+
385+
// 7. Store auto-login credentials so the server view can log in automatically
386+
await window.desktop.storeAutoLogin(serverUrl, email, HARDCODED_PASSWORD);
387+
388+
// 8. Done — pass server URL and initial path to skip Mattermost landing pages
389+
onConnect({url: serverUrl, name: resolvedOrg.name, initialPath: `/${teamName}/channels/town-square`});
333390
} catch (err) {
334391
setSetupError(err instanceof Error ? err.message : 'Something went wrong');
335392
setStep('username');

src/types/config.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ export type UniqueServer = Server & {
1515
id?: string;
1616
isPredefined?: boolean;
1717
isLoggedIn?: boolean;
18+
initialPath?: string;
1819
}
1920

2021
export type UniqueView = {

0 commit comments

Comments
 (0)