Compare commits

..

4 Commits

14 changed files with 135 additions and 82 deletions
+2 -1
View File
@@ -5,6 +5,7 @@ import { Redirect } from "react-router-dom";
import useCurrentUser from "~/hooks/useCurrentUser";
import useStores from "~/hooks/useStores";
import { changeLanguage } from "~/utils/language";
import { logoutPath } from "~/utils/routeHelpers";
import LoadingIndicator from "./LoadingIndicator";
type Props = {
@@ -32,7 +33,7 @@ const Authenticated = ({ children }: Props) => {
}
void auth.logout(true);
return <Redirect to="/" />;
return <Redirect to={logoutPath()} />;
};
export default observer(Authenticated);
+1 -6
View File
@@ -45,10 +45,6 @@ export const NativeInput = styled.input<{
${ellipsis()}
${undraggableOnDesktop()}
&[readOnly] {
color: ${s("textSecondary")};
}
&:disabled,
&::placeholder {
color: ${s("placeholder")};
@@ -130,14 +126,13 @@ export interface Props
React.InputHTMLAttributes<HTMLInputElement | HTMLTextAreaElement>,
"prefix"
> {
type?: "text" | "email" | "checkbox" | "search" | "textarea" | "password";
type?: "text" | "email" | "checkbox" | "search" | "textarea";
labelHidden?: boolean;
label?: string;
flex?: boolean;
short?: boolean;
margin?: string | number;
error?: string;
rows?: number;
/** Optional component that appears inside the input before the textarea and any icon */
prefix?: React.ReactNode;
/** Optional icon that appears inside the input before the textarea */
+26
View File
@@ -0,0 +1,26 @@
import { observer } from "mobx-react";
import * as React from "react";
import styled from "styled-components";
import { s } from "@shared/styles";
import Flex from "~/components/Flex";
type Props = {
children?: React.ReactNode;
label: React.ReactNode | string;
};
const Labeled: React.FC<Props> = ({ label, children, ...props }: Props) => (
<Flex column {...props}>
<Label>{label}</Label>
{children}
</Flex>
);
export const Label = styled(Flex)`
font-weight: 500;
padding-bottom: 4px;
display: inline-block;
color: ${s("text")};
`;
export default observer(Labeled);
+8 -15
View File
@@ -9,7 +9,7 @@ import { s } from "@shared/styles";
import { AttachmentPreset } from "@shared/types";
import { AttachmentValidation } from "@shared/validations";
import RootStore from "~/stores/RootStore";
import ButtonLarge from "~/components/ButtonLarge";
import Button from "~/components/Button";
import Flex from "~/components/Flex";
import LoadingIndicator from "~/components/LoadingIndicator";
import Modal from "~/components/Modal";
@@ -95,14 +95,8 @@ class ImageUpload extends React.Component<RootStore & Props> {
renderCropping() {
const { ui, submitText } = this.props;
return (
<Modal
onRequestClose={this.handleClose}
fullscreen={false}
title={<>&nbsp;</>}
isOpen
>
<Modal isOpen onRequestClose={this.handleClose} title="">
<Flex auto column align="center" justify="center">
{this.isUploading && <LoadingIndicator />}
<AvatarEditorContainer>
@@ -128,14 +122,9 @@ class ImageUpload extends React.Component<RootStore & Props> {
defaultValue="1"
onChange={this.handleZoom}
/>
<br />
<ButtonLarge
fullwidth
onClick={this.handleCrop}
disabled={this.isUploading}
>
<CropButton onClick={this.handleCrop} disabled={this.isUploading}>
{this.isUploading ? "Uploading…" : submitText}
</ButtonLarge>
</CropButton>
</Flex>
</Modal>
);
@@ -191,4 +180,8 @@ const RangeInput = styled.input`
}
`;
const CropButton = styled(Button)`
width: 300px;
`;
export default withStores(ImageUpload);
+1 -1
View File
@@ -306,7 +306,7 @@ export default class AuthStore extends Store<Team> {
// if this logout was forced from an authenticated route then
// save the current path so we can go back there once signed in
if (savePath) {
setPostLoginPath(window.location.pathname + window.location.search);
setPostLoginPath(window.location.pathname);
}
if (tryRevokingToken) {
+3 -5
View File
@@ -49,10 +49,8 @@ export function redirectTo(url: string) {
/**
* Check if the path is a valid path for redirect after login.
*
* @param input A path potentially including query string
* @param path
* @returns boolean indicating if the path is a valid redirect
*/
export const isAllowedLoginRedirect = (input: string) => {
const path = input.split("?")[0];
return !["/", "/create", "/home", "/logout", "/auth/"].includes(path);
};
export const isAllowedLoginRedirect = (path: string) =>
!["/", "/create", "/home", "/logout", "/auth/"].includes(path);
+5 -5
View File
@@ -153,7 +153,7 @@
"koa-useragent": "^4.1.0",
"lodash": "^4.17.21",
"mailparser": "^3.7.2",
"mammoth": "^1.9.0",
"mammoth": "^1.8.0",
"markdown-it": "^13.0.2",
"markdown-it-container": "^3.0.0",
"markdown-it-emoji": "^2.0.0",
@@ -183,7 +183,7 @@
"prosemirror-dropcursor": "^1.8.1",
"prosemirror-gapcursor": "^1.3.2",
"prosemirror-history": "^1.4.1",
"prosemirror-inputrules": "^1.5.0",
"prosemirror-inputrules": "^1.4.0",
"prosemirror-keymap": "^1.2.2",
"prosemirror-markdown": "^1.13.2",
"prosemirror-model": "^1.25.0",
@@ -232,7 +232,7 @@
"socket.io": "^4.8.1",
"socket.io-client": "^4.8.1",
"socket.io-redis": "^6.1.1",
"sonner": "^1.7.4",
"sonner": "^1.7.1",
"stoppable": "^1.1.0",
"string-replace-to-array": "^2.1.1",
"styled-components": "^5.3.11",
@@ -248,7 +248,7 @@
"uuid": "^8.3.2",
"validator": "13.12.0",
"vaul": "^1.1.2",
"vite": "^5.4.17",
"vite": "^5.4.16",
"vite-plugin-pwa": "^0.20.3",
"winston": "^3.17.0",
"ws": "^7.5.10",
@@ -296,7 +296,7 @@
"@types/markdown-it-emoji": "^2.0.4",
"@types/mime-types": "^2.1.4",
"@types/natural-sort": "^0.0.24",
"@types/node": "20.17.30",
"@types/node": "20.17.27",
"@types/node-fetch": "^2.6.9",
"@types/nodemailer": "^6.4.17",
"@types/passport-oauth2": "^1.4.17",
+13 -11
View File
@@ -28,7 +28,7 @@ if (env.AZURE_CLIENT_ID && env.AZURE_CLIENT_SECRET) {
{
clientID: env.AZURE_CLIENT_ID,
clientSecret: env.AZURE_CLIENT_SECRET,
callbackURL: ,
callbackURL: `${env.URL}/auth/azure.callback`,
useCommonEndpoint: env.AZURE_TENANT_ID ? false : true,
tenant: env.AZURE_TENANT_ID ? env.AZURE_TENANT_ID : undefined,
passReqToCallback: true,
@@ -57,12 +57,12 @@ if (env.AZURE_CLIENT_ID && env.AZURE_CLIENT_SECRET) {
const [profileResponse, organizationResponse] = await Promise.all([
// Load the users profile from the Microsoft Graph API
// https://docs.microsoft.com/en-us/graph/api/resources/users?view=graph-rest-1.0
request("GET", , accessToken),
request("GET", `https://graph.microsoft.com/v1.0/me`, accessToken),
// Load the organization profile from the Microsoft Graph API
// https://docs.microsoft.com/en-us/graph/api/organization-get?view=graph-rest-1.0
request(
"GET",
,
`https://graph.microsoft.com/v1.0/organization`,
accessToken
),
]);
@@ -73,6 +73,14 @@ if (env.AZURE_CLIENT_ID && env.AZURE_CLIENT_SECRET) {
);
}
if (!organizationResponse?.value?.length) {
throw MicrosoftGraphError(
`Unable to load organization info from Microsoft Graph API: ${organizationResponse.error?.message}`
);
}
const organization = organizationResponse.value[0];
// Note: userPrincipalName is last here for backwards compatibility with
// previous versions of Outline that did not include it.
const email =
@@ -92,13 +100,7 @@ if (env.AZURE_CLIENT_ID && env.AZURE_CLIENT_SECRET) {
const domain = parseEmail(email).domain;
const subdomain = slugifyDomain(domain);
// Try to get organization display name, but don't fail if it's not available
let teamName;
if (organizationResponse?.value?.length) {
const organization = organizationResponse.value[0];
teamName = organization.displayName;
}
const teamName = organization.displayName;
const result = await accountProvisioner({
ip: ctx.ip,
team: {
@@ -135,7 +137,7 @@ if (env.AZURE_CLIENT_ID && env.AZURE_CLIENT_SECRET) {
config.id,
passport.authenticate(config.id, { prompt: "select_account" })
);
router.get(, passportMiddleware(config.id));
router.get(`${config.id}.callback`, passportMiddleware(config.id));
}
export default router;
+3 -3
View File
@@ -43,7 +43,7 @@ type Props = {
*/
teamId?: string;
/** The displayed name of the team */
name?: string;
name: string;
/** The domain name from the email of the user logging in */
domain?: string;
/** The preferred subdomain to provision for the team if not yet created */
@@ -203,7 +203,7 @@ async function provisionFirstCollection(team: Team, user: User) {
const collection = await Collection.create(
{
name: "Welcome",
description: ,
description: `This collection is a quick guide to what ${env.APP_NAME} is all about. Feel free to delete this collection once your team is up to speed with the basics!`,
teamId: team.id,
createdById: user.id,
sort: Collection.DEFAULT_SORT,
@@ -225,7 +225,7 @@ async function provisionFirstCollection(team: Team, user: User) {
for (const title of onboardingDocs) {
const text = await readFile(
path.join(process.cwd(), "server", "onboarding", ),
path.join(process.cwd(), "server", "onboarding", `${title}.md`),
"utf8"
);
const document = await Document.create(
+3 -6
View File
@@ -6,7 +6,7 @@ import { Team, Event } from "@server/models";
type Props = {
/** The displayed name of the team */
name?: string;
name: string;
/** The domain name from the email of the user logging in */
domain?: string;
/** The preferred subdomain to provision for the team if not yet created */
@@ -38,12 +38,9 @@ async function teamCreator({
avatarUrl = null;
}
// Use subdomain as a fallback for team name if not provided
const teamName = name || subdomain;
const team = await Team.create(
{
name: teamName,
name,
avatarUrl,
authenticationProviders,
} as Partial<InferCreationAttributes<Team>>,
@@ -93,7 +90,7 @@ async function findAvailableSubdomain(team: Team, requestedSubdomain: string) {
if (existing) {
// subdomain was invalid or already used, try another
subdomain = ;
subdomain = `${normalizedSubdomain}${++append}`;
} else {
break;
}
+1 -1
View File
@@ -22,7 +22,7 @@ type Props = {
*/
teamId?: string;
/** The displayed name of the team */
name?: string;
name: string;
/** The domain name from the email of the user logging in */
domain?: string;
/** The preferred subdomain to provision for the team if not yet created */
+1 -1
View File
@@ -192,7 +192,7 @@ describe("userProvisioner", () => {
it("should prefer isAdmin argument over defaultUserRole", async () => {
const team = await buildTeam({
defaultUserRole: UserRole.Viewer,
defaultUserRole: "viewer",
});
const authenticationProviders = await team.$get("authenticationProviders");
const authenticationProvider = authenticationProviders[0];
+43 -2
View File
@@ -3,13 +3,14 @@ import compact from "lodash/compact";
import flatten from "lodash/flatten";
import isEqual from "lodash/isEqual";
import uniq from "lodash/uniq";
import { Node, DOMSerializer, Fragment } from "prosemirror-model";
import { Node, DOMSerializer, Fragment, Mark } from "prosemirror-model";
import * as React from "react";
import { renderToString } from "react-dom/server";
import styled, { ServerStyleSheet, ThemeProvider } from "styled-components";
import { prosemirrorToYDoc } from "y-prosemirror";
import * as Y from "yjs";
import EditorContainer from "@shared/editor/components/Styles";
import embeds from "@shared/editor/embeds";
import GlobalStyles from "@shared/styles/globals";
import light from "@shared/styles/theme";
import { MentionType, ProsemirrorData } from "@shared/types";
@@ -60,7 +61,47 @@ export class ProsemirrorHelper {
);
}
const node = parser.parse(input);
let node = parser.parse(input);
// in the editor embeds are created at runtime by converting links into
// embeds where they match.Because we're converting to a CRDT structure on
// the server we need to mimic this behavior.
function urlsToEmbeds(node: Node): Node {
if (node.type.name === "paragraph") {
for (const textNode of node.content.content) {
for (const embed of embeds) {
if (
textNode.text &&
textNode.marks.some(
(m: Mark) =>
m.type.name === "link" && m.attrs.href === textNode.text
) &&
embed.matcher(textNode.text)
) {
return schema.nodes.embed.createAndFill({
href: textNode.text,
}) as Node;
}
}
}
}
if (node.content) {
const contentAsArray =
node.content instanceof Fragment
? node.content.content
: node.content;
// @ts-expect-error content
node.content = Fragment.fromArray(contentAsArray.map(urlsToEmbeds));
}
return node;
}
if (node) {
node = urlsToEmbeds(node);
}
return node ? prosemirrorToYDoc(node, fieldName) : new Y.Doc();
}
+25 -25
View File
@@ -5315,10 +5315,10 @@
"@types/node" "*"
form-data "^4.0.0"
"@types/node@*", "@types/node@20.17.30", "@types/node@>=10.0.0", "@types/node@>=13.7.0":
version "20.17.30"
resolved "https://registry.yarnpkg.com/@types/node/-/node-20.17.30.tgz#1d93f656d3b869dbef7b796568ac457606ba58d0"
integrity sha512-7zf4YyHA+jvBNfVrk2Gtvs6x7E8V+YDW05bNfG2XkWDJfYRXrTiP/DsB2zSYTaHX0bGIujTBQdMVAhb+j7mwpg==
"@types/node@*", "@types/node@20.17.27", "@types/node@>=10.0.0", "@types/node@>=13.7.0":
version "20.17.27"
resolved "https://registry.yarnpkg.com/@types/node/-/node-20.17.27.tgz#dbf0f9e6f905e9004045742f94e8413e20bad776"
integrity sha512-U58sbKhDrthHlxHRJw7ZLiLDZGmAUOZUbpw0S6nL27sYUdhvgBLCRu/keSd6qcTsfArd1sRFCCBxzWATGr/0UA==
dependencies:
undici-types "~6.19.2"
@@ -11618,10 +11618,10 @@ loose-envify@^1.0.0, loose-envify@^1.1.0, loose-envify@^1.2.0, loose-envify@^1.3
dependencies:
js-tokens "^3.0.0 || ^4.0.0"
lop@^0.4.2:
version "0.4.2"
resolved "https://registry.yarnpkg.com/lop/-/lop-0.4.2.tgz#c9c2f958a39b9da1c2f36ca9ad66891a9fe84640"
integrity sha512-RefILVDQ4DKoRZsJ4Pj22TxE3omDO47yFpkIBoDKzkqPRISs5U1cnAdg/5583YPkWPaLIYHOKRMQSvjFsO26cw==
lop@^0.4.1:
version "0.4.1"
resolved "https://registry.yarnpkg.com/lop/-/lop-0.4.1.tgz#744f1696ef480e68ce1947fe557b09db5af2a738"
integrity "sha1-dE8Wlu9IDmjOGUf+VXsJ21rypzg= sha512-9xyho9why2A2tzm5aIcMWKvzqKsnxrf9B5I+8O30olh6lQU8PH978LqZoI4++37RBgS1Em5i54v1TFs/3wnmXQ=="
dependencies:
duck "^0.1.12"
option "~0.2.1"
@@ -11737,10 +11737,10 @@ makeerror@1.0.12:
dependencies:
tmpl "1.0.5"
mammoth@^1.9.0:
version "1.9.0"
resolved "https://registry.yarnpkg.com/mammoth/-/mammoth-1.9.0.tgz#71e34ca280735275788bfe95e653a058dcab4df2"
integrity sha512-F+0NxzankQV9XSUAuVKvkdQK0GbtGGuqVnND9aVf9VSeUA82LQa29GjLqYU6Eez8LHqSJG3eGiDW3224OKdpZg==
mammoth@^1.8.0:
version "1.8.0"
resolved "https://registry.yarnpkg.com/mammoth/-/mammoth-1.8.0.tgz#d8f1b0d3a0355fda129270346e9dc853f223028f"
integrity sha512-pJNfxSk9IEGVpau+tsZFz22ofjUsl2mnA5eT8PjPs2n0BP+rhVte4Nez6FdgEuxv3IGI3afiV46ImKqTGDVlbA==
dependencies:
"@xmldom/xmldom" "^0.8.6"
argparse "~1.0.3"
@@ -11748,7 +11748,7 @@ mammoth@^1.9.0:
bluebird "~3.4.0"
dingbat-to-unicode "^1.0.1"
jszip "^3.7.1"
lop "^0.4.2"
lop "^0.4.1"
path-is-absolute "^1.0.0"
underscore "^1.13.1"
xmlbuilder "^10.0.0"
@@ -13090,10 +13090,10 @@ prosemirror-history@^1.4.1:
prosemirror-view "^1.31.0"
rope-sequence "^1.3.0"
prosemirror-inputrules@^1.5.0:
version "1.5.0"
resolved "https://registry.yarnpkg.com/prosemirror-inputrules/-/prosemirror-inputrules-1.5.0.tgz#e22bfaf1d6ea4fe240ad447c184af3d520d43c37"
integrity sha512-K0xJRCmt+uSw7xesnHmcn72yBGTbY45vm8gXI4LZXbx2Z0jwh5aF9xrGQgrVPu0WbyFVFF3E/o9VhJYz6SQWnA==
prosemirror-inputrules@^1.4.0:
version "1.4.0"
resolved "https://registry.yarnpkg.com/prosemirror-inputrules/-/prosemirror-inputrules-1.4.0.tgz#ef1519bb2cb0d1e0cec74bad1a97f1c1555068bb"
integrity sha512-6ygpPRuTJ2lcOXs9JkefieMst63wVJBgHZGl5QOytN7oSZs3Co/BYbc3Yx9zm9H37Bxw8kVzCnDsihsVsL4yEg==
dependencies:
prosemirror-state "^1.0.0"
prosemirror-transform "^1.0.0"
@@ -14378,10 +14378,10 @@ socket.io@^4.8.1:
socket.io-adapter "~2.5.2"
socket.io-parser "~4.2.4"
sonner@^1.7.4:
version "1.7.4"
resolved "https://registry.yarnpkg.com/sonner/-/sonner-1.7.4.tgz#4c39820db86623800a17115c8970796aa862133a"
integrity sha512-DIS8z4PfJRbIyfVFDVnK9rO3eYDtse4Omcm6bt0oEr5/jtLgysmjuBl1frJ9E/EQZrFmKx2A8m/s5s9CRXIzhw==
sonner@^1.7.1:
version "1.7.1"
resolved "https://registry.yarnpkg.com/sonner/-/sonner-1.7.1.tgz#737110a3e6211d8d766442076f852ddde1725205"
integrity sha512-b6LHBfH32SoVasRFECrdY8p8s7hXPDn3OHUFbZZbiB1ctLS9Gdh6rpX2dVrpQA0kiL5jcRzDDldwwLkSKk3+QQ==
sort-keys@^5.0.0:
version "5.0.0"
@@ -15610,10 +15610,10 @@ vite-plugin-static-copy@^0.17.0:
fs-extra "^11.1.0"
picocolors "^1.0.0"
vite@^5.4.17:
version "5.4.17"
resolved "https://registry.yarnpkg.com/vite/-/vite-5.4.17.tgz#4bf61dd4cdbf64b0d6661f5dba76954cc81d5082"
integrity sha512-5+VqZryDj4wgCs55o9Lp+p8GE78TLVg0lasCH5xFZ4jacZjtqZa6JUw9/p0WeAojaOfncSM6v77InkFPGnvPvg==
vite@^5.4.16:
version "5.4.16"
resolved "https://registry.yarnpkg.com/vite/-/vite-5.4.16.tgz#471983257a890ef33f2700cbbbc2134f2d08abf1"
integrity sha512-Y5gnfp4NemVfgOTDQAunSD4346fal44L9mszGGY/e+qxsRT5y1sMlS/8tiQ8AFAp+MFgYNSINdfEchJiPm41vQ==
dependencies:
esbuild "^0.21.3"
postcss "^8.4.43"