mirror of
https://github.com/gristlabs/grist-core.git
synced 2026-03-02 04:09:24 +00:00
(core) Implement DiscourseConnect to enable easy sign-in to community forum
Summary: - Update cookie module, to support modern sameSite settings - Add a new cookie, grist_sid_status with less-sensitive value, to let less-trusted subdomains know if user is signed in - The new cookie is kept in-sync with the session cookie. - For a user signed in once, allow auto-signin is appropriate. - For a user signed in with multiple accounts, show a page to select which account to use. - Move css stylings for rendering users to a separate module. Test Plan: Added a test case with a simulated Discourse page to test redirects and account-selection page. Reviewers: paulfitz Reviewed By: paulfitz Differential Revision: https://phab.getgrist.com/D3047
This commit is contained in:
@@ -2,7 +2,8 @@ import {copyToClipboard} from 'app/client/lib/copyToClipboard';
|
||||
import {DocPageModel} from 'app/client/models/DocPageModel';
|
||||
import {urlState} from 'app/client/models/gristUrlState';
|
||||
import {createUserImage} from 'app/client/ui/UserImage';
|
||||
import * as um from 'app/client/ui/UserManager';
|
||||
import {cssMemberImage, cssMemberListItem, cssMemberPrimary,
|
||||
cssMemberSecondary, cssMemberText} from 'app/client/ui/UserItem';
|
||||
import {basicButton, basicButtonLink} from 'app/client/ui2018/buttons';
|
||||
import {colors, testId} from 'app/client/ui2018/cssVars';
|
||||
import {icon} from 'app/client/ui2018/icons';
|
||||
@@ -24,14 +25,14 @@ const roleNames: {[role: string]: string} = {
|
||||
function buildUserRow(user: UserAccessData, currentUser: FullUser|null, ctl: IOpenController) {
|
||||
const isCurrentUser = Boolean(currentUser && user.id === currentUser.id);
|
||||
return cssUserItem(
|
||||
um.cssMemberImage(
|
||||
cssMemberImage(
|
||||
createUserImage(user, 'large')
|
||||
),
|
||||
um.cssMemberText(
|
||||
um.cssMemberPrimary(user.name || dom('span', user.email),
|
||||
cssMemberText(
|
||||
cssMemberPrimary(user.name || dom('span', user.email),
|
||||
cssRole('(', roleNames[user.access!] || user.access, ')', testId('acl-user-access')),
|
||||
),
|
||||
user.name ? um.cssMemberSecondary(user.email) : null
|
||||
user.name ? cssMemberSecondary(user.email) : null
|
||||
),
|
||||
basicButton(cssUserButton.cls(''), icon('Copy'), 'Copy Email',
|
||||
testId('acl-user-copy'),
|
||||
@@ -84,7 +85,7 @@ const cssUsers = styled('div', `
|
||||
max-width: unset;
|
||||
`);
|
||||
|
||||
const cssUserItem = styled(um.cssMemberListItem, `
|
||||
const cssUserItem = styled(cssMemberListItem, `
|
||||
width: auto;
|
||||
padding: 8px 16px;
|
||||
align-items: center;
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import {getHomeUrl, reportError} from 'app/client/models/AppModel';
|
||||
import {BillingModel} from 'app/client/models/BillingModel';
|
||||
import {createUserImage} from 'app/client/ui/UserImage';
|
||||
import * as um from 'app/client/ui/UserManager';
|
||||
import {cssEmailInput, cssEmailInputContainer, cssMailIcon, cssMemberBtn, cssMemberImage, cssMemberListItem,
|
||||
cssMemberPrimary, cssMemberSecondary, cssMemberText, cssRemoveIcon} from 'app/client/ui/UserItem';
|
||||
import {bigPrimaryButton} from 'app/client/ui2018/buttons';
|
||||
import {testId} from 'app/client/ui2018/cssVars';
|
||||
import {normalizeEmail} from 'app/common/emails';
|
||||
@@ -39,9 +40,9 @@ export class BillingPlanManagers extends Disposable {
|
||||
dom.forEach(this._managers, manager => this._buildManagerRow(manager)),
|
||||
),
|
||||
cssEmailInputRow(
|
||||
um.cssEmailInputContainer({style: `flex: 1 1 0; margin: 0 7px 0 0;`},
|
||||
um.cssMailIcon('Mail'),
|
||||
this._emailElem = um.cssEmailInput(this._email, {onInput: true, isValid: this._isValid},
|
||||
cssEmailInputContainer({style: `flex: 1 1 0; margin: 0 7px 0 0;`},
|
||||
cssMailIcon('Mail'),
|
||||
this._emailElem = cssEmailInput(this._email, {onInput: true, isValid: this._isValid},
|
||||
{type: "email", placeholder: "Enter email address"},
|
||||
dom.on('keyup', (e: KeyboardEvent) => {
|
||||
switch (e.keyCode) {
|
||||
@@ -51,8 +52,8 @@ export class BillingPlanManagers extends Disposable {
|
||||
}),
|
||||
dom.boolAttr('disabled', this._loading)
|
||||
),
|
||||
um.cssEmailInputContainer.cls('-green', enableAdd),
|
||||
um.cssEmailInputContainer.cls('-disabled', this._loading),
|
||||
cssEmailInputContainer.cls('-green', enableAdd),
|
||||
cssEmailInputContainer.cls('-disabled', this._loading),
|
||||
testId('bpm-manager-new')
|
||||
),
|
||||
bigPrimaryButton('Add Billing Contact',
|
||||
@@ -66,17 +67,17 @@ export class BillingPlanManagers extends Disposable {
|
||||
|
||||
private _buildManagerRow(manager: FullUser) {
|
||||
const isCurrentUser = this._currentValidUser && manager.id === this._currentValidUser.id;
|
||||
return um.cssMemberListItem({style: 'width: auto;'},
|
||||
um.cssMemberImage(
|
||||
return cssMemberListItem({style: 'width: auto;'},
|
||||
cssMemberImage(
|
||||
createUserImage(manager, 'large')
|
||||
),
|
||||
um.cssMemberText(
|
||||
um.cssMemberPrimary(manager.name || dom('span', manager.email, testId('bpm-email'))),
|
||||
manager.name ? um.cssMemberSecondary(manager.email, testId('bpm-email')) : null
|
||||
cssMemberText(
|
||||
cssMemberPrimary(manager.name || dom('span', manager.email, testId('bpm-email'))),
|
||||
manager.name ? cssMemberSecondary(manager.email, testId('bpm-email')) : null
|
||||
),
|
||||
um.cssMemberBtn(
|
||||
um.cssRemoveIcon('Remove', testId('bpm-manager-delete')),
|
||||
um.cssMemberBtn.cls('-disabled', (use) => Boolean(use(this._loading) || isCurrentUser)),
|
||||
cssMemberBtn(
|
||||
cssRemoveIcon('Remove', testId('bpm-manager-delete')),
|
||||
cssMemberBtn.cls('-disabled', (use) => Boolean(use(this._loading) || isCurrentUser)),
|
||||
// Click handler.
|
||||
dom.on('click', () => this._loading.get() || isCurrentUser || this._remove(manager))
|
||||
),
|
||||
|
||||
120
app/client/ui/UserItem.ts
Normal file
120
app/client/ui/UserItem.ts
Normal file
@@ -0,0 +1,120 @@
|
||||
import {colors, vars} from 'app/client/ui2018/cssVars';
|
||||
import {icon} from 'app/client/ui2018/icons';
|
||||
import {input, styled} from 'grainjs';
|
||||
import {cssMenuItem} from 'popweasel';
|
||||
|
||||
// Styled elements used for rendering a user, e.g. in the UserManager, Billing, etc.
|
||||
// There is a general structure, but enough small variation that there is no helper at this point.
|
||||
//
|
||||
// cssMemberListItem(
|
||||
// cssMemberImage(
|
||||
// createUserImage(getFullUser(member), 'large')
|
||||
// ),
|
||||
// cssMemberText(
|
||||
// cssMemberPrimary(NAME),
|
||||
// cssMemberSecondary(EMAIL),
|
||||
// )
|
||||
// )
|
||||
|
||||
export const cssMemberListItem = styled('div', `
|
||||
display: flex;
|
||||
width: 460px;
|
||||
height: 64px;
|
||||
margin: 0 auto;
|
||||
padding: 12px 0;
|
||||
`);
|
||||
|
||||
export const cssMemberImage = styled('div', `
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
margin: 0 4px;
|
||||
border-radius: 20px;
|
||||
background-color: ${colors.lightGreen};
|
||||
background-size: cover;
|
||||
|
||||
.${cssMemberListItem.className}-removed & {
|
||||
opacity: 0.4;
|
||||
}
|
||||
`);
|
||||
|
||||
export const cssMemberText = styled('div', `
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
margin: 2px 12px;
|
||||
flex: 1 1 0;
|
||||
min-width: 0px;
|
||||
font-size: ${vars.mediumFontSize};
|
||||
|
||||
.${cssMemberListItem.className}-removed & {
|
||||
opacity: 0.4;
|
||||
}
|
||||
`);
|
||||
|
||||
export const cssMemberPrimary = styled('span', `
|
||||
font-weight: bold;
|
||||
color: ${colors.dark};
|
||||
padding: 2px 0;
|
||||
|
||||
.${cssMenuItem.className}-sel & {
|
||||
color: white;
|
||||
}
|
||||
`);
|
||||
|
||||
export const cssMemberSecondary = styled('span', `
|
||||
color: ${colors.slate};
|
||||
/* the following just undo annoying bootstrap styles that apply to all labels */
|
||||
margin: 0px;
|
||||
font-weight: normal;
|
||||
padding: 2px 0;
|
||||
white-space: nowrap;
|
||||
|
||||
.${cssMenuItem.className}-sel & {
|
||||
color: white;
|
||||
}
|
||||
`);
|
||||
|
||||
export const cssMemberBtn = styled('div', `
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
cursor: pointer;
|
||||
|
||||
&-disabled {
|
||||
opacity: 0.3;
|
||||
cursor: default;
|
||||
}
|
||||
`);
|
||||
|
||||
export const cssRemoveIcon = styled(icon, `
|
||||
margin: 12px 0;
|
||||
`);
|
||||
|
||||
export const cssEmailInputContainer = styled('div', `
|
||||
position: relative;
|
||||
display: flex;
|
||||
height: 42px;
|
||||
padding: 0 3px;
|
||||
margin: 16px 63px;
|
||||
border: 1px solid ${colors.darkGrey};
|
||||
border-radius: 3px;
|
||||
font-size: ${vars.mediumFontSize};
|
||||
outline: none;
|
||||
|
||||
&-green {
|
||||
border: 1px solid ${colors.lightGreen};
|
||||
}
|
||||
`);
|
||||
|
||||
export const cssEmailInput = styled(input, `
|
||||
flex: 1 1 0;
|
||||
line-height: 42px;
|
||||
font-size: ${vars.mediumFontSize};
|
||||
font-family: ${vars.fontFamily};
|
||||
outline: none;
|
||||
border: none;
|
||||
`);
|
||||
|
||||
export const cssMailIcon = styled(icon, `
|
||||
margin: 12px 8px 12px 13px;
|
||||
background-color: ${colors.slate};
|
||||
`);
|
||||
@@ -10,7 +10,7 @@ import * as roles from 'app/common/roles';
|
||||
import {tbind} from 'app/common/tbind';
|
||||
import {PermissionData, UserAPI} from 'app/common/UserAPI';
|
||||
import {computed, Computed, Disposable, observable, Observable} from 'grainjs';
|
||||
import {dom, DomElementArg, input, styled} from 'grainjs';
|
||||
import {dom, DomElementArg, styled} from 'grainjs';
|
||||
import {cssMenuItem} from 'popweasel';
|
||||
|
||||
import {copyToClipboard} from 'app/client/lib/copyToClipboard';
|
||||
@@ -24,6 +24,8 @@ import {getResourceParent, ResourceType} from 'app/client/models/UserManagerMode
|
||||
import {shadowScroll} from 'app/client/ui/shadowScroll';
|
||||
import {showTransientTooltip} from 'app/client/ui/tooltips';
|
||||
import {createUserImage, cssUserImage} from 'app/client/ui/UserImage';
|
||||
import {cssEmailInput, cssEmailInputContainer, cssMailIcon, cssMemberBtn, cssMemberImage, cssMemberListItem,
|
||||
cssMemberPrimary, cssMemberSecondary, cssMemberText, cssRemoveIcon} from 'app/client/ui/UserItem';
|
||||
import {basicButton, bigBasicButton, bigPrimaryButton} from 'app/client/ui2018/buttons';
|
||||
import {colors, testId, vars} from 'app/client/ui2018/cssVars';
|
||||
import {icon} from 'app/client/ui2018/icons';
|
||||
@@ -451,27 +453,6 @@ const cssOptionBtn = styled('span', `
|
||||
cursor: pointer;
|
||||
`);
|
||||
|
||||
export const cssMemberListItem = styled('div', `
|
||||
display: flex;
|
||||
width: 460px;
|
||||
height: 64px;
|
||||
margin: 0 auto;
|
||||
padding: 12px 0;
|
||||
`);
|
||||
|
||||
export const cssMemberImage = styled('div', `
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
margin: 0 4px;
|
||||
border-radius: 20px;
|
||||
background-color: ${colors.lightGreen};
|
||||
background-size: cover;
|
||||
|
||||
.${cssMemberListItem.className}-removed & {
|
||||
opacity: 0.4;
|
||||
}
|
||||
`);
|
||||
|
||||
const cssPublicMemberIcon = styled(icon, `
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
@@ -479,88 +460,6 @@ const cssPublicMemberIcon = styled(icon, `
|
||||
--icon-color: ${colors.lightGreen};
|
||||
`);
|
||||
|
||||
export const cssMemberText = styled('div', `
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
margin: 2px 12px;
|
||||
flex: 1 1 0;
|
||||
min-width: 0px;
|
||||
font-size: ${vars.mediumFontSize};
|
||||
|
||||
.${cssMemberListItem.className}-removed & {
|
||||
opacity: 0.4;
|
||||
}
|
||||
`);
|
||||
|
||||
export const cssMemberPrimary = styled('span', `
|
||||
font-weight: bold;
|
||||
color: ${colors.dark};
|
||||
padding: 2px 0;
|
||||
|
||||
.${cssMenuItem.className}-sel & {
|
||||
color: white;
|
||||
}
|
||||
`);
|
||||
|
||||
export const cssMemberSecondary = styled('span', `
|
||||
color: ${colors.slate};
|
||||
/* the following just undo annoying bootstrap styles that apply to all labels */
|
||||
margin: 0px;
|
||||
font-weight: normal;
|
||||
padding: 2px 0;
|
||||
white-space: nowrap;
|
||||
|
||||
.${cssMenuItem.className}-sel & {
|
||||
color: white;
|
||||
}
|
||||
`);
|
||||
|
||||
export const cssEmailInputContainer = styled('div', `
|
||||
position: relative;
|
||||
display: flex;
|
||||
height: 42px;
|
||||
padding: 0 3px;
|
||||
margin: 16px 63px;
|
||||
border: 1px solid ${colors.darkGrey};
|
||||
border-radius: 3px;
|
||||
font-size: ${vars.mediumFontSize};
|
||||
outline: none;
|
||||
|
||||
&-green {
|
||||
border: 1px solid ${colors.lightGreen};
|
||||
}
|
||||
`);
|
||||
|
||||
export const cssMailIcon = styled(icon, `
|
||||
margin: 12px 8px 12px 13px;
|
||||
background-color: ${colors.slate};
|
||||
`);
|
||||
|
||||
export const cssEmailInput = styled(input, `
|
||||
flex: 1 1 0;
|
||||
line-height: 42px;
|
||||
font-size: ${vars.mediumFontSize};
|
||||
font-family: ${vars.fontFamily};
|
||||
outline: none;
|
||||
border: none;
|
||||
`);
|
||||
|
||||
export const cssMemberBtn = styled('div', `
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
cursor: pointer;
|
||||
|
||||
&-disabled {
|
||||
opacity: 0.3;
|
||||
cursor: default;
|
||||
}
|
||||
`);
|
||||
|
||||
export const cssRemoveIcon = styled(icon, `
|
||||
margin: 12px 0;
|
||||
`);
|
||||
|
||||
const cssUndoIcon = styled(icon, `
|
||||
margin: 12px 0;
|
||||
`);
|
||||
|
||||
@@ -8,7 +8,10 @@ import { AppHeader } from 'app/client/ui/AppHeader';
|
||||
import * as BillingPageCss from "app/client/ui/BillingPageCss";
|
||||
import * as forms from "app/client/ui/forms";
|
||||
import { pagePanels } from "app/client/ui/PagePanels";
|
||||
import { bigBasicButton, bigBasicButtonLink, bigPrimaryButton, bigPrimaryButtonLink,
|
||||
import { createUserImage } from 'app/client/ui/UserImage';
|
||||
import { cssMemberImage, cssMemberListItem, cssMemberPrimary,
|
||||
cssMemberSecondary, cssMemberText } from 'app/client/ui/UserItem';
|
||||
import { basicButtonLink, bigBasicButton, bigBasicButtonLink, bigPrimaryButton, bigPrimaryButtonLink,
|
||||
cssButton } from "app/client/ui2018/buttons";
|
||||
import { colors, mediaSmall, testId, vars } from "app/client/ui2018/cssVars";
|
||||
import { getOrgName, Organization } from "app/common/UserAPI";
|
||||
@@ -92,6 +95,7 @@ export class WelcomePage extends Disposable {
|
||||
state.welcome === 'user' ? dom.create(this._buildNameForm.bind(this)) :
|
||||
state.welcome === 'info' ? dom.create(this._buildInfoForm.bind(this)) :
|
||||
state.welcome === 'teams' ? dom.create(this._buildOrgPicker.bind(this)) :
|
||||
state.welcome === 'select-account' ? dom.create(this._buildAccountPicker.bind(this)) :
|
||||
null
|
||||
)),
|
||||
));
|
||||
@@ -336,8 +340,50 @@ export class WelcomePage extends Disposable {
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private _buildAccountPicker(): DomContents {
|
||||
function addUserToLink(email: string): string {
|
||||
const next = new URLSearchParams(location.search).get('next') || '';
|
||||
const url = new URL(next, location.href);
|
||||
url.searchParams.set('user', email);
|
||||
return url.toString();
|
||||
}
|
||||
|
||||
return [
|
||||
cssParagraph(
|
||||
"Select an account to continue with.",
|
||||
),
|
||||
dom.maybe(this._appModel.topAppModel.users, users =>
|
||||
users.map(user => basicButtonLink(
|
||||
cssUserItem.cls(''),
|
||||
cssMemberListItem(
|
||||
cssMemberImage(
|
||||
createUserImage(user, 'large')
|
||||
),
|
||||
cssMemberText(
|
||||
cssMemberPrimary(user.name || dom('span', user.email, testId('select-email'))),
|
||||
user.name ? cssMemberSecondary(user.email, testId('select-email')) : null
|
||||
),
|
||||
),
|
||||
{href: addUserToLink(user.email)},
|
||||
testId('select-user'),
|
||||
)),
|
||||
),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
const cssUserItem = styled('div', `
|
||||
margin: 0 0 8px;
|
||||
align-items: center;
|
||||
&:first-of-type {
|
||||
margin-top: 16px;
|
||||
}
|
||||
&:hover {
|
||||
background-color: ${colors.lightGrey};
|
||||
}
|
||||
`);
|
||||
|
||||
const cssScrollContainer = styled('div', `
|
||||
display: flex;
|
||||
overflow-y: auto;
|
||||
|
||||
Reference in New Issue
Block a user