Переглянути джерело

handle info set for new oauth users

Justin Rhee 3 роки тому
батько
коміт
3914b67e9c

+ 5 - 4
api/server/handlers/user/create.go

@@ -40,10 +40,11 @@ func (u *UserCreateHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
 	}
 
 	user := &models.User{
-		Email:     request.Email,
-		Password:  request.Password,
-		FirstName: request.FirstName,
-		LastName:  request.LastName,
+		Email:       request.Email,
+		Password:    request.Password,
+		FirstName:   request.FirstName,
+		LastName:    request.LastName,
+		CompanyName: request.CompanyName,
 	}
 
 	// check if user exists

+ 49 - 0
api/server/handlers/user/update_user_info.go

@@ -0,0 +1,49 @@
+package user
+
+import (
+	"net/http"
+
+	"github.com/porter-dev/porter/api/server/handlers"
+	"github.com/porter-dev/porter/api/server/shared"
+	"github.com/porter-dev/porter/api/server/shared/apierrors"
+	"github.com/porter-dev/porter/api/server/shared/config"
+	"github.com/porter-dev/porter/api/types"
+	"github.com/porter-dev/porter/internal/models"
+)
+
+type UpdateUserInfoHandler struct {
+	handlers.PorterHandlerReadWriter
+}
+
+func NewUpdateUserInfoHandler(
+	config *config.Config,
+	decoderValidator shared.RequestDecoderValidator,
+	writer shared.ResultWriter,
+) *UpdateUserInfoHandler {
+	return &UpdateUserInfoHandler{
+		PorterHandlerReadWriter: handlers.NewDefaultPorterHandler(config, decoderValidator, writer),
+	}
+}
+
+func (v *UpdateUserInfoHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
+	user, _ := r.Context().Value(types.UserScope).(*models.User)
+
+	request := &types.UpdateUserInfoRequest{}
+	if ok := v.DecodeAndValidate(w, r, request); !ok {
+		return
+	}
+
+	if request.FirstName != "" && request.LastName != "" && request.CompanyName != "" {
+		user.FirstName = request.FirstName
+		user.LastName = request.LastName
+		user.CompanyName = request.CompanyName
+	}
+
+	user, err := v.Repo().User().UpdateUser(user)
+	if err != nil {
+		v.HandleAPIError(w, r, apierrors.NewErrInternal(err))
+		return
+	}
+
+	v.WriteResult(w, r, user.ToUserType())
+}

+ 25 - 0
api/server/router/user.go

@@ -121,6 +121,31 @@ func getUserRoutes(
 		Router:   r,
 	})
 
+	// POST /api/users/update/info -> user.UpdateUserInfoHandler
+	updateUserInfoEndpoint := factory.NewAPIEndpoint(
+		&types.APIRequestMetadata{
+			Verb:   types.APIVerbUpdate,
+			Method: types.HTTPVerbPost,
+			Path: &types.Path{
+				Parent:       basePath,
+				RelativePath: "/users/update/info",
+			},
+			Scopes: []types.PermissionScope{types.UserScope},
+		},
+	)
+
+	updateUserInfoHandler := user.NewUpdateUserInfoHandler(
+		config,
+		factory.GetDecoderValidator(),
+		factory.GetResultWriter(),
+	)
+
+	routes = append(routes, &router.Route{
+		Endpoint: updateUserInfoEndpoint,
+		Handler:  updateUserInfoHandler,
+		Router:   r,
+	})
+
 	// GET /api/users/current -> user.NewUserGetCurrentHandler
 	authCheckEndpoint := factory.NewAPIEndpoint(
 		&types.APIRequestMetadata{

+ 12 - 4
api/types/user.go

@@ -6,13 +6,15 @@ type User struct {
 	EmailVerified bool   `json:"email_verified"`
 	FirstName     string `json:"first_name"`
 	LastName      string `json:"last_name"`
+	CompanyName   string `json:"company_name"`
 }
 
 type CreateUserRequest struct {
-	Email     string `json:"email" form:"required,max=255,email"`
-	Password  string `json:"password" form:"required,max=255"`
-	FirstName string `json:"first_name" form:"required,max=255"`
-	LastName  string `json:"last_name" form:"required,max=255"`
+	Email       string `json:"email" form:"required,max=255,email"`
+	Password    string `json:"password" form:"required,max=255"`
+	FirstName   string `json:"first_name" form:"required,max=255"`
+	LastName    string `json:"last_name" form:"required,max=255"`
+	CompanyName string `json:"company_name" form:"required,max=255"`
 }
 
 type CreateUserResponse User
@@ -72,3 +74,9 @@ type WelcomeWebhookRequest struct {
 	Role      string `json:"role" schema:"role"`
 	Name      string `json:"name" schema:"name"`
 }
+
+type UpdateUserInfoRequest struct {
+	FirstName   string `json:"first_name" form:"required,max=255"`
+	LastName    string `json:"last_name" form:"required,max=255"`
+	CompanyName string `json:"company_name" form:"required,max=255"`
+}

+ 33 - 2
dashboard/src/main/Main.tsx

@@ -8,6 +8,7 @@ import ResetPasswordFinalize from "./auth/ResetPasswordFinalize";
 import Login from "./auth/Login";
 import Register from "./auth/Register";
 import VerifyEmail from "./auth/VerifyEmail";
+import SetInfo from "./auth/SetInfo";
 import CurrentError from "./CurrentError";
 import Home from "./home/Home";
 import Loading from "components/Loading";
@@ -19,8 +20,10 @@ type StateType = {
   loading: boolean;
   isLoggedIn: boolean;
   isEmailVerified: boolean;
+  hasInfo: boolean;
   initialized: boolean;
   local: boolean;
+  userId: number;
 };
 
 export default class Main extends Component<PropsType, StateType> {
@@ -28,8 +31,10 @@ export default class Main extends Component<PropsType, StateType> {
     loading: true,
     isLoggedIn: false,
     isEmailVerified: false,
+    hasInfo: false,
     initialized: localStorage.getItem("init") === "true",
     local: false,
+    userId: null as number,
   };
 
   componentDidMount() {
@@ -41,11 +46,12 @@ export default class Main extends Component<PropsType, StateType> {
       .checkAuth("", {}, {})
       .then((res) => {
         if (res && res?.data) {
-          setUser(res?.data?.id, res?.data?.email);
+          setUser(res.data.id, res.data.email);
           this.setState({
             isLoggedIn: true,
-            isEmailVerified: res?.data?.email_verified,
+            isEmailVerified: res.data.email_verified,
             initialized: true,
+            hasInfo: res.data.company_name && true,
             loading: false,
           });
         } else {
@@ -79,6 +85,7 @@ export default class Main extends Component<PropsType, StateType> {
             isLoggedIn: true,
             isEmailVerified: res?.data?.email_verified,
             initialized: true,
+            hasInfo: res.data.company_name && true,
             loading: false,
           });
         } else {
@@ -126,6 +133,30 @@ export default class Main extends Component<PropsType, StateType> {
       );
     }
 
+    // Handle case where new user signs up via OAuth and has not set name and company
+    if (
+      this.context.capabilities.version === "production" &&
+      !this.state.hasInfo && 
+      this.state.userId > 9312 &&
+      this.state.isLoggedIn
+    ) {
+      return (
+        <Switch>
+          <Route
+            path="/"
+            render={() => {
+              return (
+                <SetInfo 
+                  handleLogOut={this.handleLogOut}
+                  authenticate={this.authenticate}
+                />
+              );
+            }}
+          />
+        </Switch>
+      )
+    }
+
     return (
       <Switch>
         <Route

+ 1 - 1
dashboard/src/main/auth/Login.tsx

@@ -193,7 +193,7 @@ const Login: React.FC<Props> = ({
               }}
               width="100%"
               height="40px"
-              error={(emailError && "Please enter a valid email") || (credentialError && "")}
+              error={emailError && "Please enter a valid email"}
             />
             <Spacer y={1} />
             <Input

+ 34 - 2
dashboard/src/main/auth/Register.tsx

@@ -34,9 +34,12 @@ const Register: React.FC<Props> = ({
   const [firstNameError, setFirstNameError] = useState(false);
   const [lastName, setLastName] = useState("");
   const [lastNameError, setLastNameError] = useState(false);
+  const [companyName, setCompanyName] = useState("");
+  const [companyNameError, setCompanyNameError] = useState(false);
   const [email, setEmail] = useState("");
-  const [password, setPassword] = useState("");
   const [emailError, setEmailError] = useState(false);
+  const [password, setPassword] = useState("");
+  const [passwordError, setPasswordError] = useState(false);
   const [hasBasic, setHasBasic] = useState(true);
   const [hasGithub, setHasGithub] = useState(true);
   const [hasGoogle, setHasGoogle] = useState(false);
@@ -55,8 +58,22 @@ const Register: React.FC<Props> = ({
       setLastNameError(true);
     }
 
+    if (password === "") {
+      setPasswordError(true);
+    }
+
+    if (companyName === "") {
+      setCompanyNameError(true);
+    }
+
     // Check for valid input
-    if (emailRegex.test(email) && firstName !== "" && lastName !== "") {
+    if (
+      emailRegex.test(email) && 
+      firstName !== "" &&
+      lastName !== "" &&
+      password !== "" &&
+      companyName !== ""
+    ) {
       // Attempt user registration
       api
         .registerUser(
@@ -66,6 +83,7 @@ const Register: React.FC<Props> = ({
             password: password,
             first_name: firstName,
             last_name: lastName,
+            company_name: companyName,
           },
           {}
         )
@@ -216,6 +234,19 @@ const Register: React.FC<Props> = ({
               </RowWrapper>
             </Container>
             <Spacer y={1} />
+            <Input
+              placeholder="Company name"
+              label="Company name"
+              value={companyName}
+              setValue={(x) => {
+                setCompanyName(x);
+                setCompanyNameError(false);
+              }}
+              width="100%"
+              height="40px"
+              error={(companyNameError && "")}
+            />
+            <Spacer y={1} />
             <Input
               type="email"
               placeholder="Email"
@@ -238,6 +269,7 @@ const Register: React.FC<Props> = ({
               width="100%"
               height="40px"
               type="password"
+              error={(passwordError && "")}
             />
             <Spacer height="30px" />
             <Button onClick={handleRegister} width="100%" height="40px">

+ 316 - 0
dashboard/src/main/auth/SetInfo.tsx

@@ -0,0 +1,316 @@
+import React, { useEffect, useState, useContext } from "react";
+import styled from "styled-components";
+
+import github from "assets/github-icon.png";
+import logo from "assets/logo.png";
+import GoogleIcon from "assets/GoogleIcon";
+
+import api from "shared/api";
+import { emailRegex } from "shared/regex";
+import { Context } from "shared/Context";
+
+import Heading from "components/form-components/Heading";
+import Button from "components/porter/Button";
+import Container from "components/porter/Container";
+import Input from "components/porter/Input";
+import Spacer from "components/porter/Spacer";
+import Text from "components/porter/Text";
+import Link from "components/porter/Link";
+
+type Props = {
+  authenticate: () => void;
+  handleLogOut: () => void;
+};
+
+const getWindowDimensions = () => {
+  const { innerWidth: width, innerHeight: height } = window;
+  return { width, height };
+}
+
+const SetInfo: React.FC<Props> = ({
+  authenticate,
+  handleLogOut,
+}) => {
+  const { user, setCurrentError } = useContext(Context);
+  const [firstName, setFirstName] = useState("");
+  const [firstNameError, setFirstNameError] = useState(false);
+  const [lastName, setLastName] = useState("");
+  const [lastNameError, setLastNameError] = useState(false);
+  const [companyName, setCompanyName] = useState("");
+  const [companyNameError, setCompanyNameError] = useState(false);
+  const [windowDimensions, setWindowDimensions] = useState(getWindowDimensions());
+
+  const handleResize = () => {
+    setWindowDimensions(getWindowDimensions());
+  };
+
+  const finishAccountSetup = async () => {
+    if (firstName === "") {
+      setFirstNameError(true);
+    }
+
+    if (lastName === "") {
+      setLastNameError(true);
+    }
+
+    if (companyName === "") {
+      setCompanyNameError(true);
+    }
+
+    if (
+      firstName !== "" &&
+      lastName !== "" &&
+      companyName !== ""
+    ) {
+      api.updateUserInfo(
+        "",
+        { 
+          first_name: firstName,
+          last_name: lastName,
+          company_name: companyName,
+        },
+        { id: user.id }
+      )
+        .then((res: any) => {
+          authenticate();
+        })
+        .catch((err) => setCurrentError(err));
+    }
+  };
+
+  const handleKeyDown = (e: any) => {
+    if (e.key === "Enter") {
+      finishAccountSetup();
+    };
+  };
+
+  // Manually re-register event listener on email/password change
+  useEffect(() => {
+    document.removeEventListener("keydown", handleKeyDown);
+    document.addEventListener("keydown", handleKeyDown);
+    return () => {
+      document.removeEventListener("keydown", handleKeyDown);
+    };
+  }, [firstName, lastName, companyName]);
+
+  useEffect(() => {
+    window.addEventListener('resize', handleResize);
+    return () => window.removeEventListener('resize', handleResize);
+  }, []);
+
+  return (
+    <StyledRegister>
+      {windowDimensions.width > windowDimensions.height && (
+        <Wrapper>
+          <Logo src={logo} />
+          <Spacer y={2} />
+          <Jumbotron>
+            Deploy and scale <Shiny>effortlessly</Shiny> with Porter
+          </Jumbotron>
+          <Spacer y={2} />
+          <CheckRow>
+            <i className="material-icons">done</i> Generous free tier for small teams
+          </CheckRow>
+          <Spacer y={0.5} />
+          <CheckRow>
+            <i className="material-icons">done</i> Bring your own cloud (and cloud credits)
+          </CheckRow>
+          <Spacer y={0.5} />
+          <CheckRow>
+            <i className="material-icons">done</i> Fully automated setup and deployment
+          </CheckRow>
+        </Wrapper>
+      )}
+      <Wrapper>
+        {windowDimensions.width <= windowDimensions.height && (
+          <Flex>
+            <Logo src={logo} />
+            <Spacer y={2} />
+          </Flex>
+        )}
+        <Heading isAtTop>
+          Finish setting up your account
+        </Heading>
+        <Spacer y={1} />
+        <Container row>
+          <RowWrapper>
+            <Input
+              placeholder="First name"
+              label="First name"
+              value={firstName}
+              setValue={(x) => {
+                setFirstName(x);
+                setFirstNameError(false);
+              }}
+              width="100%"
+              height="40px"
+              error={(firstNameError && "First name cannot be blank")}
+            />
+            {!firstNameError && lastNameError && (
+              <Spacer height="27px" />
+            )}
+          </RowWrapper>
+          <Spacer inline x={2} />
+          <RowWrapper>
+            <Input
+              placeholder="Last name"
+              label="Last name"
+              value={lastName}
+              setValue={(x) => {
+                setLastName(x);
+                setLastNameError(false);
+              }}
+              width="100%"
+              height="40px"
+              error={(lastNameError && "Last name cannot be blank")}
+            />
+            {!lastNameError && firstNameError && (
+              <Spacer height="27px" />
+            )}
+          </RowWrapper>
+        </Container>
+        <Spacer y={1} />
+        <Input
+          placeholder="Company name"
+          label="Company name"
+          value={companyName}
+          setValue={(x) => {
+            setCompanyName(x);
+            setCompanyNameError(false);
+          }}
+          width="100%"
+          height="40px"
+          error={(companyNameError && "")}
+        />
+        <Spacer height="30px" />
+        <Button onClick={finishAccountSetup} width="100%" height="40px">
+          Continue
+        </Button>
+        <Spacer y={1} />
+        <Text 
+          size={13}
+          color="helper"
+        >
+          Want to use a different login method? <Link onClick={handleLogOut}>Log out</Link>
+        </Text>
+      </Wrapper>
+    </StyledRegister>
+  );
+};
+
+export default SetInfo;
+
+const RowWrapper = styled.div`
+  width: 100%;
+`;
+
+const Flex = styled.div`
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  flex-direction: column;
+`;
+
+const CheckRow = styled.div`
+  font-size: 14px;
+  display: flex;
+  align-items: center;
+  color: #aaaabb;
+  > i {
+    font-size: 18px;
+    margin-right: 10px;
+    float: left;
+    color: #4797ff;
+  }
+`;
+
+const Shiny = styled.span`
+  background-image: linear-gradient(225deg, #fff, #7980ff);
+  -webkit-background-clip: text;
+  background-clip: text;
+  -webkit-text-fill-color: transparent;
+`;
+
+const Jumbotron = styled.div`
+  font-size: 32px;
+  font-weight: 500;
+  line-height: 1.5;
+`;
+
+const Logo = styled.img`
+  height: 24px;
+  user-select: none;
+`;
+
+const StyledGoogleIcon = styled(GoogleIcon)`
+  width: 38px;
+  height: 38px;
+`;
+
+const Line = styled.div`
+  height: 2px;
+  width: 100%;
+  background: #ffffff22;
+  margin: 35px 0px 30px;
+`;
+
+const Or = styled.div`
+  position: absolute;
+  width: 50px;
+  text-align: center;
+  background: #111114;
+  z-index: 999;
+  left: calc(50% - 25px);
+  margin-top: -1px;
+`;
+
+const OrWrapper = styled.div`
+  display: flex;
+  align-items: center;
+  color: #ffffff44;
+  font-size: 14px;
+  position: relative;
+`;
+
+const Icon = styled.img`
+  height: 18px;
+  margin: 14px;
+`;
+
+const OAuthButton = styled.div`
+  width: 100%;
+  height: 40px;
+  display: flex;
+  background: #ffffff;
+  align-items: center;
+  border-radius: 5px;
+  color: #000000;
+  cursor: pointer;
+  user-select: none;
+  font-weight: 500;
+  font-size: 13px;
+  :hover {
+    background: #ffffffdd;
+  }
+`;
+
+const Wrapper = styled.div`
+  width: 500px;
+  margin-top: -20px;
+  position: relative;
+  padding: 25px;
+  border-radius: 5px;
+  font-size: 13px;
+`;
+
+const StyledRegister = styled.div`
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  width: 100vw;
+  height: 100vh;
+  position: fixed;
+  top: 0;
+  left: 0;
+  background: #111114;
+`;

+ 1 - 0
dashboard/src/main/auth/VerifyEmail.tsx

@@ -144,6 +144,7 @@ const Email = styled.div`
   display: flex;
   align-items: center;
   padding: 15px;
+  color: #aaaabb;
 `;
 
 const Flex = styled.div`

+ 13 - 0
dashboard/src/shared/api.tsx

@@ -1395,6 +1395,7 @@ const registerUser = baseApi<{
   password: string;
   first_name: string;
   last_name: string;
+  company_name: string;
 }>("POST", "/api/users");
 
 const rollbackChart = baseApi<
@@ -1425,6 +1426,17 @@ const uninstallTemplate = baseApi<
   return `/api/projects/${id}/clusters/${cluster_id}/namespaces/${namespace}/releases/${name}/0`;
 });
 
+const updateUserInfo = baseApi<
+  {
+    first_name: string;
+    last_name: string;
+    company_name: string;
+  },
+  {}
+>("POST", (pathParams) => {
+  return `/api/users/update/info`;
+});
+
 const updateUser = baseApi<
   {
     rawKubeConfig?: string;
@@ -2525,6 +2537,7 @@ export default {
   registerUser,
   rollbackChart,
   uninstallTemplate,
+  updateUserInfo,
   updateUser,
   renameConfigMap,
   updateConfigMap,

+ 6 - 2
internal/models/user.go

@@ -13,8 +13,9 @@ type User struct {
 	Password      string `json:"password"`
 	EmailVerified bool   `json:"email_verified"`
 
-	FirstName string `json:"first_name"`
-	LastName  string `json:"last_name"`
+	FirstName   string `json:"first_name"`
+	LastName    string `json:"last_name"`
+	CompanyName string `json:"company_name"`
 
 	// ID of oauth integration for github connection (optional)
 	GithubAppIntegrationID uint
@@ -30,5 +31,8 @@ func (u *User) ToUserType() *types.User {
 		ID:            u.ID,
 		Email:         u.Email,
 		EmailVerified: u.EmailVerified,
+		FirstName:     u.FirstName,
+		LastName:      u.LastName,
+		CompanyName:   u.CompanyName,
 	}
 }