diff --git a/app/config.ts b/app/config.ts index 2bf8a9d36f..e14703c20c 100644 --- a/app/config.ts +++ b/app/config.ts @@ -3,6 +3,8 @@ const config = { canvasPath: "/canvas", designerPath: "/designer", dashboardPath: "/dashboard", + loginPath: "/login", + logoutPath: "/logout", }; export default config; export type Config = typeof config; diff --git a/app/critical-css.ts b/app/critical-css.ts index a10c10bf04..06e22d2d86 100644 --- a/app/critical-css.ts +++ b/app/critical-css.ts @@ -10,6 +10,7 @@ const getCssTextFunctions = { [config.canvasPath]: getCanvasCssText, [config.designerPath]: getDesignerCssText, [config.dashboardPath]: getDesignerCssText, + [config.loginPath]: getDesignerCssText, }; const getCssTextFunction = (url: string) => { diff --git a/app/routes/auth/github.tsx b/app/routes/auth/github.tsx index f0b627356c..5b5cfd855c 100644 --- a/app/routes/auth/github.tsx +++ b/app/routes/auth/github.tsx @@ -1,8 +1,10 @@ -import { ActionFunction, LoaderFunction, redirect } from "@remix-run/node"; +import { ActionFunction } from "@remix-run/node"; +import config from "~/config"; import { authenticator } from "~/services/auth.server"; -export const loader: LoaderFunction = () => redirect("/login"); - export const action: ActionFunction = ({ request }) => { - return authenticator.authenticate("github", request); + return authenticator.authenticate("github", request, { + successRedirect: config.dashboardPath, + failureRedirect: config.loginPath, + }); }; diff --git a/app/routes/auth/github/callback.tsx b/app/routes/auth/github/callback.tsx index 5b77ad6add..3b821aaeb3 100644 --- a/app/routes/auth/github/callback.tsx +++ b/app/routes/auth/github/callback.tsx @@ -1,9 +1,10 @@ import { LoaderFunction } from "@remix-run/node"; +import config from "~/config"; import { authenticator } from "~/services/auth.server"; export const loader: LoaderFunction = ({ request }) => { return authenticator.authenticate("github", request, { - successRedirect: "/dashboard", - failureRedirect: "/login", + successRedirect: config.dashboardPath, + failureRedirect: config.loginPath, }); }; diff --git a/app/routes/auth/google.tsx b/app/routes/auth/google.tsx new file mode 100644 index 0000000000..c45f660304 --- /dev/null +++ b/app/routes/auth/google.tsx @@ -0,0 +1,8 @@ +import { ActionFunction, LoaderFunction, redirect } from "@remix-run/node"; +import { authenticator } from "~/services/auth.server"; + +export const loader: LoaderFunction = () => redirect("/login"); + +export const action: ActionFunction = ({ request }) => { + return authenticator.authenticate("google", request); +}; diff --git a/app/routes/auth/google/callback.tsx b/app/routes/auth/google/callback.tsx new file mode 100644 index 0000000000..adbafcf44e --- /dev/null +++ b/app/routes/auth/google/callback.tsx @@ -0,0 +1,10 @@ +import { LoaderFunction } from "@remix-run/node"; +import config from "~/config"; +import { authenticator } from "~/services/auth.server"; + +export const loader: LoaderFunction = ({ request }) => { + return authenticator.authenticate("google", request, { + successRedirect: config.dashboardPath, + failureRedirect: config.loginPath, + }); +}; diff --git a/app/routes/dashboard/index.tsx b/app/routes/dashboard/index.tsx index 886434ee40..9345c2f584 100644 --- a/app/routes/dashboard/index.tsx +++ b/app/routes/dashboard/index.tsx @@ -31,9 +31,9 @@ export const action: ActionFunction = async ({ request }) => { export const loader: LoaderFunction = async ({ request }) => { const user = await authenticator.isAuthenticated(request); - console.log(user); + console.log(user) if (!user) { - return redirect("/login"); + return redirect(config.loginPath); } const { userId, headers } = await ensureUserCookie(request); const projects = await db.project.loadManyByUserId(userId); diff --git a/app/routes/login.tsx b/app/routes/login.tsx index f1e543c7b4..f1efd71239 100644 --- a/app/routes/login.tsx +++ b/app/routes/login.tsx @@ -1,9 +1,92 @@ +import { Links, LiveReload, Meta, Scripts } from "@remix-run/react"; +import { + LinksFunction, + LoaderFunction, + MetaFunction, + redirect, +} from "@remix-run/node"; +import { CriticalCss } from "@webstudio-is/sdk"; +import { darkTheme } from "~/shared/design-system"; import { Form } from "@remix-run/react"; +import { Button, Card, Flex, Heading } from "~/shared/design-system"; +import interStyles from "~/shared/font-faces/inter.css"; -export default function Login() { +import { GoogleIcon, GithubIcon } from "~/shared/icons"; +import { authenticator } from "~/services/auth.server"; +import config from "~/config"; + +export const links: LinksFunction = () => { + return [ + { + rel: "stylesheet", + href: interStyles, + }, + ]; +}; + +export const meta: MetaFunction = () => { + return { title: "Webstudio Login" }; +}; + +const RootDesignerRoute = () => { return ( -
- -
+ + + + + + + + + + + + + Login + + +
+ +
+
+ +
+
+
+
+
+ + {process.env.NODE_ENV === "development" && } + + ); -} +}; + +export const loader: LoaderFunction = async ({ request }) => { + const user = await authenticator.isAuthenticated(request); + if (user) { + return redirect(config.dashboardPath); + } + + return {}; +}; + +export default RootDesignerRoute; diff --git a/app/services/auth.server.ts b/app/services/auth.server.ts index 4c4ebb7214..d7cf054928 100644 --- a/app/services/auth.server.ts +++ b/app/services/auth.server.ts @@ -1,28 +1,49 @@ import { User } from "@prisma/client"; import { Authenticator } from "remix-auth"; import { GitHubStrategy } from "remix-auth-github"; +import { GoogleStrategy } from "remix-auth-google"; import { sessionStorage } from "~/services/session.server"; -import { createOrLoginWithGithub } from "~/shared/db/user.server"; +import { + createOrLoginWithGithub, + createOrLoginWithGoogle, +} from "~/shared/db/user.server"; + +const url = `${ + process.env.VERCEL_URL + ? `https://${process.env.VERCEL_URL}` + : "http://localhost:3000" +}/auth`; + +const github = new GitHubStrategy( + { + clientID: process.env.GH_CLIENT_ID as string, + clientSecret: process.env.GH_CLIENT_SECRET as string, + callbackURL: `${url}/github/callback`, + }, + async ({ profile }) => { + const user = createOrLoginWithGithub(profile); + + return user; + } +); + +const google = new GoogleStrategy( + { + clientID: "YOUR_CLIENT_ID", + clientSecret: "YOUR_CLIENT_SECRET", + callbackURL: `${url}/google/callback`, + }, + async ({ profile }) => { + const user = createOrLoginWithGoogle(profile); + + return user; + } +); // Create an instance of the authenticator, pass a generic with what // strategies will return and will store in the session export const authenticator = new Authenticator(sessionStorage); if (process.env.GH_CLIENT_ID && process.env.GH_CLIENT_SECRET) { - authenticator.use( - new GitHubStrategy( - { - clientID: process.env.GH_CLIENT_ID as string, - clientSecret: process.env.GH_CLIENT_SECRET as string, - callbackURL: `https://${ - process.env.VERCEL_URL || "192.168.1.91:3000" - }/auth/github/callback`, - }, - async ({ profile }) => { - const user = createOrLoginWithGithub(profile); - - return user; - } - ), - "github" - ); + authenticator.use(github, "github"); + authenticator.use(google, "google"); } diff --git a/app/shared/db/user.server.ts b/app/shared/db/user.server.ts index eddd8633a0..1965064bd7 100644 --- a/app/shared/db/user.server.ts +++ b/app/shared/db/user.server.ts @@ -1,5 +1,7 @@ import { User } from "@prisma/client"; import { GitHubProfile } from "remix-auth-github"; +import { GoogleProfile } from "remix-auth-google"; +import { ensureUserCookie } from "../session"; import { prisma } from "./prisma.server"; export const createOrLoginWithGithub = async ( @@ -24,3 +26,26 @@ export const createOrLoginWithGithub = async ( return newUser; }; + +export const createOrLoginWithGoogle = async ( + profile: GoogleProfile +): Promise => { + const user = await prisma.user.findUnique({ + where: { email: profile._json.email }, + }); + + if (user) { + return user; + } + + const newUser = prisma.user.create({ + data: { + email: profile._json.email, + username: profile.displayName, + image: profile._json.avatar_url, + provider: profile.provider, + }, + }); + + return newUser; +}; diff --git a/app/shared/icons/github.tsx b/app/shared/icons/github.tsx new file mode 100644 index 0000000000..734c467c7f --- /dev/null +++ b/app/shared/icons/github.tsx @@ -0,0 +1,24 @@ +import * as React from "react"; +import { IconProps } from "./types"; + +export const GithubIcon = React.forwardRef( + ({ color = "currentColor", ...props }, forwardedRef) => { + return ( + + GitHub + + + ); + } +); + +GithubIcon.displayName = "GithubIcon"; diff --git a/app/shared/icons/google.tsx b/app/shared/icons/google.tsx new file mode 100644 index 0000000000..f1312f932c --- /dev/null +++ b/app/shared/icons/google.tsx @@ -0,0 +1,24 @@ +import * as React from "react"; +import { IconProps } from "./types"; + +export const GoogleIcon = React.forwardRef( + ({ color = "currentColor", ...props }, forwardedRef) => { + return ( + + Google + + + ); + } +); + +GoogleIcon.displayName = "GoogleIcon"; diff --git a/app/shared/icons/index.ts b/app/shared/icons/index.ts index b6af274862..09c273be06 100644 --- a/app/shared/icons/index.ts +++ b/app/shared/icons/index.ts @@ -4,6 +4,8 @@ export * from "./brush"; export * from "./form"; export * from "./undo"; export * from "./redo"; +export * from "./google"; +export * from "./github"; export * from "./devices"; export * from "./tablet"; export * from "./types"; diff --git a/package.json b/package.json index 29ba5b21e5..1ca42def4c 100644 --- a/package.json +++ b/package.json @@ -72,6 +72,7 @@ "react-use": "^17.3.2", "remix-auth": "^3.2.2", "remix-auth-github": "^1.1.1", + "remix-auth-google": "^1.1.0", "slugify": "^1.6.5" }, "devDependencies": { diff --git a/prisma/migrations/20220611090740_/migration.sql b/prisma/migrations/20220611090740_/migration.sql new file mode 100644 index 0000000000..9449a3e7dc --- /dev/null +++ b/prisma/migrations/20220611090740_/migration.sql @@ -0,0 +1,19 @@ +/* + Warnings: + + - You are about to drop the column `createdAt` on the `User` table. All the data in the column will be lost. + - You are about to drop the column `email` on the `User` table. All the data in the column will be lost. + - You are about to drop the column `image` on the `User` table. All the data in the column will be lost. + - You are about to drop the column `provider` on the `User` table. All the data in the column will be lost. + - You are about to drop the column `username` on the `User` table. All the data in the column will be lost. + +*/ +-- DropIndex +DROP INDEX "User_email_key"; + +-- AlterTable +ALTER TABLE "User" DROP COLUMN "createdAt", +DROP COLUMN "email", +DROP COLUMN "image", +DROP COLUMN "provider", +DROP COLUMN "username"; diff --git a/prisma/migrations/20220611091346_add_email/migration.sql b/prisma/migrations/20220611091346_add_email/migration.sql new file mode 100644 index 0000000000..2640ef8e8e --- /dev/null +++ b/prisma/migrations/20220611091346_add_email/migration.sql @@ -0,0 +1,15 @@ +/* + Warnings: + + - A unique constraint covering the columns `[email]` on the table `User` will be added. If there are existing duplicate values, this will fail. + +*/ +-- AlterTable +ALTER TABLE "User" ADD COLUMN "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, +ADD COLUMN "email" TEXT, +ADD COLUMN "image" TEXT, +ADD COLUMN "provider" TEXT, +ADD COLUMN "username" TEXT; + +-- CreateIndex +CREATE UNIQUE INDEX "User_email_key" ON "User"("email"); diff --git a/prisma/schema.prisma b/prisma/schema.prisma index f2c4830093..1eda4ff425 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -10,6 +10,16 @@ datasource db { url = env("DATABASE_URL") } +model User { + id String @id @default(uuid()) + email String? @unique + provider String? + image String? + username String? + createdAt DateTime @default(now()) + projects Project[] +} + model Project { id String @id @default(uuid()) userId String @@ -18,10 +28,7 @@ model Project { prodTreeId String? // exists when published devTreeId String prodTreeIdHistory String @default("[]") -} - -model User { - id String @id @default(uuid()) + User User @relation(fields: [userId], references: [id]) } model Tree { diff --git a/yarn.lock b/yarn.lock index 16f46f60fa..ef6c2269e0 100644 --- a/yarn.lock +++ b/yarn.lock @@ -12144,7 +12144,14 @@ remix-auth-github@^1.1.1: remix-auth "^3.0.0" remix-auth-oauth2 "^1.0.0" -remix-auth-oauth2@^1.0.0: +remix-auth-google@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/remix-auth-google/-/remix-auth-google-1.1.0.tgz#4ffdc41b180a7a622d9008e7a25b6e409dbf5f20" + integrity sha512-yzZ8JEiSoY/cvo5eJQBIzqpnaWOjQkYCgBklCP1QwT01sdR2F066uhV+jR9Wcx0bPCntdMG4PrytVBJzWTg+BA== + dependencies: + remix-auth-oauth2 "^1.1.0" + +remix-auth-oauth2@^1.0.0, remix-auth-oauth2@^1.1.0: version "1.2.2" resolved "https://registry.yarnpkg.com/remix-auth-oauth2/-/remix-auth-oauth2-1.2.2.tgz#bebe0ac586744ed94885bd47f6bcb710d377fede" integrity sha512-bbQw/SFAbilSE1zrZFaJN/NM7WUkC63CKJFVz2e/Y21qqMZo5eVQXooSV6d23mABfodeVk6wthngdLAjxak6fQ==