mirror of
https://github.com/webstudio-is/webstudio.git
synced 2026-11-08 21:15:18 +09:00
Merge branch 'main' into 68-move-content-editable-toolbar-to-workspace-tools
This commit is contained in:
commit
5c7da8feeb
15 files changed
+287
-105
No files matched your search
+2
-1
@@ -1,4 +1,5 @@
|
||||
DATABASE_URL=postgresql://user:pass@localhost/webstudio
|
||||
AUTH_SECRET="# Linux: `openssl rand -hex 32` or go to https://generate-secret.now.sh/32"
|
||||
GH_CLIENT_SECRET=
|
||||
GH_CLIENT_ID=
|
||||
GH_CLIENT_ID=
|
||||
DEV_LOGIN=true
|
||||
@@ -3,7 +3,6 @@ on: [push]
|
||||
jobs:
|
||||
checks:
|
||||
env:
|
||||
DATABASE_URL: mongo://
|
||||
NODE_ENV: test
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
@@ -13,16 +12,7 @@ jobs:
|
||||
steps:
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@v2
|
||||
|
||||
# - name: Install deps and build (with cache)
|
||||
# uses: bahmutov/npm-install@v1
|
||||
- run: yarn install
|
||||
|
||||
- name: Next.js cache
|
||||
uses: actions/cache@v2
|
||||
with:
|
||||
path: ${{ github.workspace }}/.next/cache
|
||||
key: ${{ runner.os }}-${{ runner.node }}-${{ hashFiles('**/yarn.lock') }}-nextjs
|
||||
|
||||
- run: yarn checks
|
||||
- run: yarn build
|
||||
+16
-5
@@ -1,7 +1,14 @@
|
||||
import { LinksFunction, MetaFunction } from "@remix-run/node";
|
||||
import { Form, useLoaderData } from "@remix-run/react";
|
||||
import { useState } from "react";
|
||||
import { Button, Card, Flex, Heading, TextField } from "~/shared/design-system";
|
||||
import {
|
||||
Button,
|
||||
Card,
|
||||
Flex,
|
||||
Heading,
|
||||
Text,
|
||||
TextField,
|
||||
} from "~/shared/design-system";
|
||||
import interStyles from "~/shared/font-faces/inter.css";
|
||||
|
||||
import { GoogleIcon, GithubIcon, CommitIcon } from "~/shared/icons";
|
||||
@@ -24,7 +31,7 @@ export const meta: MetaFunction = () => {
|
||||
return { title: "Webstudio Login" };
|
||||
};
|
||||
|
||||
export const Login = () => {
|
||||
export const Login = ({ errorMessage }: { errorMessage: string }) => {
|
||||
const [isDevLogin, setIsDevLogin] = useState(false);
|
||||
const loaderData = useLoaderData();
|
||||
|
||||
@@ -35,10 +42,14 @@ export const Login = () => {
|
||||
align="center"
|
||||
justify="center"
|
||||
>
|
||||
<Card css={{ width: "$10", padding: "$5", zoom: 1.4 }} variant="active">
|
||||
<Card css={{ width: 200, padding: "$5", zoom: 1.4 }} variant="active">
|
||||
<Flex direction="column" gap="2" align="center">
|
||||
<Heading>Login</Heading>
|
||||
|
||||
{errorMessage.length ? (
|
||||
<Text css={{ textAlign: "center" }} variant="red">
|
||||
{errorMessage}
|
||||
</Text>
|
||||
) : null}
|
||||
<Flex gap="2" direction="column" align="center">
|
||||
<Form action="/auth/github" method="post">
|
||||
<Button type="submit">
|
||||
@@ -67,7 +78,7 @@ export const Login = () => {
|
||||
minLength={2}
|
||||
required
|
||||
autoFocus
|
||||
placeholder="Place your AUTH_SECRET"
|
||||
placeholder="Place your auth secret here"
|
||||
/>
|
||||
</Form>
|
||||
) : (
|
||||
|
||||
+19
-6
@@ -1,14 +1,27 @@
|
||||
import { ActionFunction } from "@remix-run/node";
|
||||
import { ActionFunction, redirect } from "@remix-run/node";
|
||||
import config from "~/config";
|
||||
import { authenticator } from "~/services/auth.server";
|
||||
import { AUTH_PROVIDERS } from "~/shared/session/useLoginErrorMessage";
|
||||
|
||||
export default function Dev() {
|
||||
return null;
|
||||
}
|
||||
|
||||
export const action: ActionFunction = ({ request }) => {
|
||||
return authenticator.authenticate("dev", request, {
|
||||
successRedirect: config.dashboardPath,
|
||||
failureRedirect: config.loginPath,
|
||||
});
|
||||
export const action: ActionFunction = async ({ request }) => {
|
||||
try {
|
||||
return await authenticator.authenticate("dev", request, {
|
||||
successRedirect: config.dashboardPath,
|
||||
throwOnError: true,
|
||||
});
|
||||
} catch (error: unknown) {
|
||||
// all redirects are basically errors and in that case we don't want to catch it
|
||||
if (error instanceof Response) return error;
|
||||
if (error instanceof Error) {
|
||||
return redirect(
|
||||
`${config.loginPath}?error=${AUTH_PROVIDERS.LOGIN_DEV}&message=${
|
||||
error?.message || ""
|
||||
}`
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -1,19 +1,39 @@
|
||||
import { ActionFunction } from "@remix-run/node";
|
||||
import { ActionFunction, redirect } from "@remix-run/node";
|
||||
import config from "~/config";
|
||||
import { authenticator } from "~/services/auth.server";
|
||||
import { sentryException } from "~/shared/sentry";
|
||||
import { ensureUserCookie } from "~/shared/session";
|
||||
import { AUTH_PROVIDERS } from "~/shared/session/useLoginErrorMessage";
|
||||
|
||||
export default function GH() {
|
||||
return null;
|
||||
}
|
||||
|
||||
export const action: ActionFunction = async ({ request }) => {
|
||||
const { userId } = await ensureUserCookie(request);
|
||||
return authenticator.authenticate("github", request, {
|
||||
context: {
|
||||
userId,
|
||||
},
|
||||
successRedirect: config.dashboardPath,
|
||||
failureRedirect: config.loginPath,
|
||||
});
|
||||
try {
|
||||
const { userId } = await ensureUserCookie(request);
|
||||
return await authenticator.authenticate("github", request, {
|
||||
context: {
|
||||
userId,
|
||||
},
|
||||
successRedirect: config.dashboardPath,
|
||||
throwOnError: true,
|
||||
});
|
||||
} catch (error: unknown) {
|
||||
// all redirects are basically errors and in that case we don't want to catch it
|
||||
if (error instanceof Response) return error;
|
||||
if (error instanceof Error) {
|
||||
sentryException({
|
||||
message: error.message,
|
||||
extra: {
|
||||
loginMethod: AUTH_PROVIDERS.LOGIN_GITHUB,
|
||||
},
|
||||
});
|
||||
return redirect(
|
||||
`${config.loginPath}?error=${AUTH_PROVIDERS.LOGIN_GITHUB}&message=${
|
||||
error?.message || ""
|
||||
}`
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -1,10 +1,21 @@
|
||||
import { LoaderFunction } from "@remix-run/node";
|
||||
import { LoaderFunction, redirect } from "@remix-run/node";
|
||||
import config from "~/config";
|
||||
import { authenticator } from "~/services/auth.server";
|
||||
import { ensureUserCookie } from "~/shared/session";
|
||||
import { AUTH_PROVIDERS } from "~/shared/session/useLoginErrorMessage";
|
||||
|
||||
export const loader: LoaderFunction = async ({ request }) => {
|
||||
const { userId } = await ensureUserCookie(request);
|
||||
const url = new URL(request.url);
|
||||
const error = url.searchParams.get("error");
|
||||
const error_description = url.searchParams.get("error_description");
|
||||
if (error) {
|
||||
return redirect(
|
||||
`${config.loginPath}?error=${AUTH_PROVIDERS.LOGIN_GITHUB}&message=${
|
||||
error_description || error
|
||||
}`
|
||||
);
|
||||
}
|
||||
return authenticator.authenticate("github", request, {
|
||||
context: {
|
||||
userId,
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import { ActionFunction, LoaderFunction, redirect } from "@remix-run/node";
|
||||
import config from "~/config";
|
||||
import { authenticator } from "~/services/auth.server";
|
||||
import { sentryException } from "~/shared/sentry";
|
||||
import { ensureUserCookie } from "~/shared/session";
|
||||
import { AUTH_PROVIDERS } from "~/shared/session/useLoginErrorMessage";
|
||||
|
||||
export default function Google() {
|
||||
return null;
|
||||
@@ -10,12 +12,30 @@ export default function Google() {
|
||||
export const loader: LoaderFunction = () => redirect("/login");
|
||||
|
||||
export const action: ActionFunction = async ({ request }) => {
|
||||
const { userId } = await ensureUserCookie(request);
|
||||
return authenticator.authenticate("google", request, {
|
||||
context: {
|
||||
userId,
|
||||
},
|
||||
successRedirect: config.dashboardPath,
|
||||
failureRedirect: config.loginPath,
|
||||
});
|
||||
try {
|
||||
const { userId } = await ensureUserCookie(request);
|
||||
return await authenticator.authenticate("google", request, {
|
||||
context: {
|
||||
userId,
|
||||
},
|
||||
successRedirect: config.dashboardPath,
|
||||
throwOnError: true,
|
||||
});
|
||||
} catch (error: unknown) {
|
||||
// all redirects are basically errors and in that case we don't want to catch it
|
||||
if (error instanceof Response) return error;
|
||||
if (error instanceof Error) {
|
||||
sentryException({
|
||||
message: error.message,
|
||||
extra: {
|
||||
loginMethod: AUTH_PROVIDERS.LOGIN_GOOGLE,
|
||||
},
|
||||
});
|
||||
return redirect(
|
||||
`${config.loginPath}?error=${AUTH_PROVIDERS.LOGIN_GOOGLE}&message=${
|
||||
error?.message || ""
|
||||
}`
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -5,6 +5,7 @@ import config from "~/config";
|
||||
import env from "~/env.server";
|
||||
|
||||
import { Login, links } from "~/auth/";
|
||||
import { useLoginErrorMessage } from "~/shared/session/useLoginErrorMessage";
|
||||
|
||||
export { links };
|
||||
|
||||
@@ -18,7 +19,8 @@ export const loader: LoaderFunction = async ({ request }) => {
|
||||
};
|
||||
|
||||
const LoginRoute = () => {
|
||||
return <Login />;
|
||||
const errorMessage = useLoginErrorMessage();
|
||||
return <Login errorMessage={errorMessage} />;
|
||||
};
|
||||
|
||||
export default LoginRoute;
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
createOrLoginWithGoogle,
|
||||
} from "~/shared/db/user.server";
|
||||
import { sentryException } from "~/shared/sentry";
|
||||
import { AUTH_PROVIDERS } from "~/shared/session/useLoginErrorMessage";
|
||||
|
||||
const url = `${
|
||||
process.env.VERCEL_URL
|
||||
@@ -49,8 +50,8 @@ const google = new GoogleStrategy(
|
||||
export const authenticator = new Authenticator<User>(sessionStorage);
|
||||
if (process.env.GH_CLIENT_ID && process.env.GH_CLIENT_SECRET) {
|
||||
authenticator.use(github, "github");
|
||||
authenticator.use(google, "google");
|
||||
}
|
||||
authenticator.use(google, "google");
|
||||
|
||||
if (process.env.DEV_LOGIN === "true") {
|
||||
authenticator.use(
|
||||
@@ -66,14 +67,14 @@ if (process.env.DEV_LOGIN === "true") {
|
||||
sentryException({
|
||||
message: error.message,
|
||||
extra: {
|
||||
loginMethod: "dev",
|
||||
loginMethod: AUTH_PROVIDERS.LOGIN_DEV,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error("Wrong code");
|
||||
throw new Error("The dev login code is incorrect");
|
||||
}),
|
||||
"dev"
|
||||
);
|
||||
|
||||
@@ -1,16 +1,17 @@
|
||||
import {
|
||||
PrismaClient,
|
||||
Prisma,
|
||||
PrismaClientKnownRequestError,
|
||||
} from "@webstudio-is/sdk/lib/prisma.server";
|
||||
import { PrismaClient, Prisma } from "@prisma/client";
|
||||
import { PrismaClientKnownRequestError } from "@prisma/client/runtime";
|
||||
|
||||
declare global {
|
||||
// allow global `var` declarations
|
||||
// eslint-disable-next-line no-var
|
||||
var prisma: PrismaClient | undefined;
|
||||
}
|
||||
|
||||
// this fixes the issue with `warn(prisma-client) There are already 10 instances of Prisma Client actively running.`
|
||||
// explanation here
|
||||
// https://www.prisma.io/docs/guides/database/troubleshooting-orm/help-articles/nextjs-prisma-client-dev-practices
|
||||
export const prisma = global.prisma || new PrismaClient();
|
||||
|
||||
if (process.env.NODE_ENV !== "production") global.prisma = prisma;
|
||||
|
||||
export const prisma = new PrismaClient();
|
||||
export { Prisma, PrismaClientKnownRequestError };
|
||||
|
||||
const main = async () => {
|
||||
await prisma.$connect();
|
||||
};
|
||||
|
||||
main().catch((error) => {
|
||||
throw error;
|
||||
});
|
||||
@@ -1,39 +0,0 @@
|
||||
import {useRef, useEffect} from 'react';
|
||||
import {numericGestureControl} from './numericGestureControl.js';
|
||||
|
||||
const useNumericGestureControl = ({ref, value, direction}) => {
|
||||
useEffect(() => {
|
||||
const {disconnectedCallback} = numericGestureControl(ref.current, {
|
||||
initialValue: ref.current.value = value,
|
||||
direction: direction,
|
||||
onValueChange: (event) => {
|
||||
event.preventDefault();
|
||||
event.target.value = event.value;
|
||||
event.target.select();
|
||||
}
|
||||
});
|
||||
return () => disconnectedCallback();
|
||||
}, [direction]);
|
||||
}
|
||||
|
||||
const Input = ({value, direction}) => {
|
||||
const ref = useRef();
|
||||
useNumericGestureControl({ref, value, direction});
|
||||
return <input defaultValue={value} ref={ref} />;
|
||||
}
|
||||
|
||||
export const NumericInput = Object.assign(Input.bind({}), {args: { value: 0, direction: 'horizontal' }});
|
||||
|
||||
export default {
|
||||
title: 'numericGestureControl',
|
||||
component: Input,
|
||||
argTypes: {
|
||||
value: {
|
||||
control: { type: 'number' },
|
||||
},
|
||||
direction: {
|
||||
options: ['horizontal', 'vertical'],
|
||||
control: { type: 'radio' },
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,61 @@
|
||||
import { useRef, useEffect, RefObject } from "react";
|
||||
import {
|
||||
numericGestureControl,
|
||||
type Value,
|
||||
type Direction,
|
||||
} from "./numericGestureControl";
|
||||
|
||||
const useNumericGestureControl = ({
|
||||
ref,
|
||||
value,
|
||||
direction,
|
||||
}: {
|
||||
ref: RefObject<HTMLInputElement>;
|
||||
value: Value;
|
||||
direction: Direction;
|
||||
}) => {
|
||||
useEffect(() => {
|
||||
if (ref.current === null) return;
|
||||
ref.current.value = String(value);
|
||||
const { disconnectedCallback } = numericGestureControl(ref.current, {
|
||||
initialValue: value,
|
||||
direction: direction,
|
||||
onValueChange: (event) => {
|
||||
event.preventDefault();
|
||||
event.target.value = String(event.value);
|
||||
event.target.select();
|
||||
},
|
||||
});
|
||||
return () => disconnectedCallback();
|
||||
}, [direction, value, ref]);
|
||||
};
|
||||
|
||||
const Input = ({
|
||||
value,
|
||||
direction,
|
||||
}: {
|
||||
value: Value;
|
||||
direction: Direction;
|
||||
}) => {
|
||||
const ref = useRef<HTMLInputElement | null>(null);
|
||||
useNumericGestureControl({ ref, value, direction });
|
||||
return <input defaultValue={value} ref={ref} />;
|
||||
};
|
||||
|
||||
export const NumericInput = Object.assign(Input.bind({}), {
|
||||
args: { value: 0, direction: "horizontal" },
|
||||
});
|
||||
|
||||
export default {
|
||||
title: "numericGestureControl",
|
||||
component: Input,
|
||||
argTypes: {
|
||||
value: {
|
||||
control: { type: "number" },
|
||||
},
|
||||
direction: {
|
||||
options: ["horizontal", "vertical"],
|
||||
control: { type: "radio" },
|
||||
},
|
||||
},
|
||||
};
|
||||
+28
-8
@@ -13,15 +13,34 @@
|
||||
* }
|
||||
* });
|
||||
*/
|
||||
|
||||
export type Direction = "horizontal" | "vertical";
|
||||
|
||||
export type Value = number;
|
||||
|
||||
type Options = {
|
||||
initialValue: Value;
|
||||
direction: Direction;
|
||||
onValueChange: (event: {
|
||||
target: HTMLInputElement;
|
||||
value: Value;
|
||||
preventDefault: () => void;
|
||||
}) => void;
|
||||
};
|
||||
|
||||
export const numericGestureControl = (
|
||||
targetNode,
|
||||
{ initialValue = 0, direction = "horizontal", onValueChange = ({}) => null }
|
||||
targetNode: HTMLInputElement,
|
||||
{
|
||||
initialValue = 0,
|
||||
direction = "horizontal",
|
||||
onValueChange = () => null,
|
||||
}: Options
|
||||
) => {
|
||||
const { ownerDocument } = targetNode;
|
||||
const eventNames = ["pointerup", "pointerdown", "pointermove"];
|
||||
const eventNames = ["pointerup", "pointerdown", "pointermove"] as const;
|
||||
const state = {
|
||||
value: initialValue,
|
||||
cursor: cursorNode.cloneNode(true),
|
||||
cursor: cursorNode.cloneNode(true) as HTMLElement,
|
||||
offset: 0,
|
||||
velocity: direction === "horizontal" ? 1 : -1,
|
||||
position: direction === "horizontal" ? "left" : "top",
|
||||
@@ -31,7 +50,7 @@ export const numericGestureControl = (
|
||||
direction === "horizontal" ? "0 1px" : "1px 0"
|
||||
} 1.1px rgba(0,0,0,.4))`,
|
||||
};
|
||||
const handleEvent = (event) => {
|
||||
const handleEvent = (event: PointerEvent) => {
|
||||
const { type, pressure, offsetX, offsetY, movementY, movementX } = event;
|
||||
const offset = direction === "horizontal" ? offsetX : offsetY;
|
||||
const movement = direction === "horizontal" ? movementX : -movementY;
|
||||
@@ -42,7 +61,6 @@ export const numericGestureControl = (
|
||||
}
|
||||
case "pointerdown": {
|
||||
const handlePointerLockEvent = () => {
|
||||
const { pointerLockElement } = ownerDocument;
|
||||
switch (ownerDocument.pointerLockElement) {
|
||||
case targetNode: {
|
||||
state.offset = offset;
|
||||
@@ -77,7 +95,9 @@ export const numericGestureControl = (
|
||||
else if (state.offset > globalThis.innerWidth) state.offset = 1;
|
||||
state.value += movement;
|
||||
state.offset += movement * state.velocity;
|
||||
state.cursor.style[state.position] = `${state.offset}px`;
|
||||
state.cursor.style[
|
||||
state.position as "top" | "left"
|
||||
] = `${state.offset}px`;
|
||||
onValueChange({
|
||||
target: targetNode,
|
||||
value: state.value,
|
||||
@@ -109,4 +129,4 @@ const cursorNode = new Range().createContextualFragment(`
|
||||
<path d="M 15 4.5L 15 2L 11.5 5.5L 15 9L 15 6.5L 31 6.5L 31 9L 34.5 5.5L 31 2L 31 4.5Z" fill="#111" fill-rule="evenodd"></path>
|
||||
</g>
|
||||
</svg>
|
||||
`).firstElementChild;
|
||||
`).firstElementChild as HTMLElement;
|
||||
@@ -0,0 +1,60 @@
|
||||
import isNil from "lodash/isNil";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useSearchParams } from "react-router-dom";
|
||||
import { sentryException } from "../sentry";
|
||||
|
||||
export const AUTH_PROVIDERS = {
|
||||
LOGIN_DEV: "login_dev",
|
||||
LOGIN_GITHUB: "login_github",
|
||||
LOGIN_GOOGLE: "login_google",
|
||||
};
|
||||
|
||||
export const LOGIN_ERROR_MESSAGES = {
|
||||
[AUTH_PROVIDERS.LOGIN_DEV]: "There has been an issue logging you in with dev",
|
||||
[AUTH_PROVIDERS.LOGIN_GITHUB]:
|
||||
"There has been an issue logging you in with Github",
|
||||
[AUTH_PROVIDERS.LOGIN_GOOGLE]:
|
||||
"There has been an issue logging you in with Google",
|
||||
};
|
||||
|
||||
export const useLoginErrorMessage = (): string => {
|
||||
const [searchParams, setSearchParams] = useSearchParams();
|
||||
const [messageToReturn, setMessageToReturn] = useState("");
|
||||
|
||||
useEffect(() => {
|
||||
const error = searchParams.get("error");
|
||||
const message = searchParams.get("message");
|
||||
|
||||
const hasMessageToShow =
|
||||
error !== null && !isNil(message) && message !== "";
|
||||
|
||||
if (hasMessageToShow) {
|
||||
sentryException({
|
||||
message: message,
|
||||
});
|
||||
setMessageToReturn(message);
|
||||
|
||||
setSearchParams({});
|
||||
return;
|
||||
}
|
||||
|
||||
switch (error) {
|
||||
case AUTH_PROVIDERS.LOGIN_DEV:
|
||||
setMessageToReturn(LOGIN_ERROR_MESSAGES[AUTH_PROVIDERS.LOGIN_DEV]);
|
||||
break;
|
||||
case AUTH_PROVIDERS.LOGIN_GITHUB:
|
||||
setMessageToReturn(LOGIN_ERROR_MESSAGES[AUTH_PROVIDERS.LOGIN_GITHUB]);
|
||||
break;
|
||||
case AUTH_PROVIDERS.LOGIN_GOOGLE:
|
||||
setMessageToReturn(LOGIN_ERROR_MESSAGES[AUTH_PROVIDERS.LOGIN_GOOGLE]);
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
setSearchParams({});
|
||||
}, [searchParams, setSearchParams]);
|
||||
|
||||
return messageToReturn;
|
||||
};
|
||||
@@ -57,3 +57,13 @@
|
||||
- Click on the canvas closes breakpoints selector
|
||||
- Styles added on each breakpoint are only applied to each breakpoint, verify that by resizing the canvas in preview mode
|
||||
- Resizing canvas width in preview mode doesn't limit resize slider to each breakpoint
|
||||
|
||||
1. Auth
|
||||
- Try to access the dashboard when not logged in to make sure you will be redirected to the login page
|
||||
- Open the login page when logged in to make sure you are redirected to the dashboard
|
||||
- Ensure you can view a built site in an incognito tab
|
||||
- Ensure you can view a forked site in an incognito tab
|
||||
- Click on dev login, add the AUTH_SECRET value in the `.env` value and make sure user is authenticated and redirected to the dashboard
|
||||
- Click on dev login and write a wrong value to make sure you get an error message and stay in the login page.
|
||||
- Click on login with GitHub and make sure that you are redirected to the dashboard
|
||||
- Click on login with Google and make sure that you are redirected to the dashboard
|
||||
Reference in new issue
Block a user