Quellcode durchsuchen

Merge pull request #6539 from weseek/feat/create-invited-page

feat: Create invitedForm
Yuki Takei vor 3 Jahren
Ursprung
Commit
8c9a0b9a1c

+ 1 - 1
packages/app/config/rate-limiter.ts

@@ -33,7 +33,7 @@ export const defaultConfig: IApiRateLimitEndpointMap = {
     maxRequests: MAX_REQUESTS_TIER_1,
     usersPerIpProspection: 100,
   },
-  '/login/activateInvited': {
+  '/invited/activateInvited': {
     method: 'POST',
     maxRequests: MAX_REQUESTS_TIER_2,
   },

+ 4 - 0
packages/app/public/static/locales/en_US/translation.json

@@ -675,6 +675,10 @@
     "Registration successful": "Registration successful",
     "Setup": "Setup"
   },
+  "invited": {
+    "discription_heading": "Create Account",
+    "discription": "Create an your account with the invited email address"
+  },
   "export_bulk": {
     "failed_to_export": "Failed to export",
     "failed_to_count_pages": "Failed to count pages",

+ 4 - 0
packages/app/public/static/locales/ja_JP/translation.json

@@ -666,6 +666,10 @@
     "Registration successful": "登録完了",
     "Setup": "セットアップ"
   },
+  "invited": {
+    "discription_heading": "アカウント作成",
+    "discription": "招待を受け取ったメールアドレスでアカウントを作成します"
+  },
   "export_bulk": {
     "failed_to_export": "ページのエクスポートに失敗しました",
     "failed_to_count_pages": "ページ数の取得に失敗しました",

+ 4 - 0
packages/app/public/static/locales/zh_CN/translation.json

@@ -722,6 +722,10 @@
 		"Registration successful": "注册成功",
 		"Setup": "安装程序"
 	},
+  "invited": {
+    "discription_heading": "创建账户",
+    "discription": "用被邀请的电子邮件地址创建一个你的账户"
+  },
   "export_bulk": {
     "failed_to_export": "导出失败",
     "failed_to_count_pages": "页面计数失败",

+ 0 - 0
packages/app/src/components/Layout/Invited.module.scss → packages/app/src/components/Invited.module.scss


+ 111 - 0
packages/app/src/components/InvitedForm.tsx

@@ -0,0 +1,111 @@
+import React from 'react';
+
+import { useTranslation } from 'next-i18next';
+
+import { useCsrfToken, useCurrentUser } from '../stores/context';
+
+export type InvitedFormProps = {
+  invitedFormUsername: string,
+  invitedFormName: string,
+}
+
+export const InvitedForm = (props: InvitedFormProps): JSX.Element => {
+  const { t } = useTranslation();
+  const { data: csrfToken } = useCsrfToken();
+  const { data: user } = useCurrentUser();
+
+  const { invitedFormUsername, invitedFormName } = props;
+
+  if (user == null) {
+    return <></>;
+  }
+
+  return (
+    <div className="noLogin-dialog p-3 mx-auto" id="noLogin-dialog">
+      <p className="alert alert-success">
+        <strong>{ t('invited.discription_heading') }</strong><br></br>
+        <small>{ t('invited.discription') }</small>
+      </p>
+      <form role="form" action="/invited/activateInvited" method="post" id="invited-form">
+        {/* Email Form */}
+        <div className="input-group">
+          <div className="input-group-prepend">
+            <span className="input-group-text">
+              <i className="icon-envelope"></i>
+            </span>
+          </div>
+          <input
+            type="text"
+            className="form-control"
+            disabled
+            placeholder={t('Email')}
+            name="invitedForm[email]"
+            defaultValue={user.email}
+            required
+          />
+        </div>
+        {/* UserID Form */}
+        <div className="input-group" id="input-group-username">
+          <div className="input-group-prepend">
+            <span className="input-group-text">
+              <i className="icon-user"></i>
+            </span>
+          </div>
+          <input
+            type="text"
+            className="form-control"
+            placeholder={t('User ID')}
+            name="invitedForm[username]"
+            value={invitedFormUsername}
+            required
+          />
+        </div>
+        {/* Name Form */}
+        <div className="input-group">
+          <div className="input-group-prepend">
+            <span className="input-group-text">
+              <i className="icon-tag"></i>
+            </span>
+          </div>
+          <input
+            type="text"
+            className="form-control"
+            placeholder={t('Name')}
+            name="invitedForm[name]"
+            value={invitedFormName}
+            required
+          />
+        </div>
+        {/* Password Form */}
+        <div className="input-group">
+          <div className="input-group-prepend">
+            <span className="input-group-text">
+              <i className="icon-lock"></i>
+            </span>
+          </div>
+          <input
+            type="password"
+            className="form-control"
+            placeholder={t('Password')}
+            name="invitedForm[password]"
+            required
+          />
+        </div>
+        {/* Create Button */}
+        <div className="input-group justify-content-center d-flex mt-5">
+          <input type="hidden" name="_csrf" value={csrfToken} />
+          <button type="submit" className="btn btn-fill" id="register">
+            <div className="eff"></div>
+            <span className="btn-label"><i className="icon-user-follow"></i></span>
+            <span className="btn-label-text">{t('Create')}</span>
+          </button>
+        </div>
+      </form>
+      <div className="input-group mt-5 d-flex justify-content-center">
+        <a href="https://growi.org" className="link-growi-org">
+          <span className="growi">GROWI</span>.<span className="org">ORG</span>
+        </a>
+      </div>
+    </div>
+  );
+};

+ 1 - 2
packages/app/src/components/Layout/NoLoginLayout.tsx

@@ -34,10 +34,9 @@ export const NoLoginLayout = ({
                     <h1 className="my-3">GROWI</h1>
                     <div className="noLogin-form-errors px-3"></div>
                   </div>
+                  {children}
                 </div>
 
-                {children}
-
               </div>
             </div>
           </div>

+ 2 - 4
packages/app/src/pages/installer.page.tsx

@@ -47,10 +47,8 @@ const InstallerPage: NextPage<Props> = (props: Props) => {
 
   return (
     <NoLoginLayout title={useCustomTitle(props, 'GROWI')} className={classNames.join(' ')}>
-      <div className="col-md-12">
-        <div id="installer-form-container">
-          <InstallerForm />
-        </div>
+      <div id="installer-form-container">
+        <InstallerForm />
       </div>
     </NoLoginLayout>
   );

+ 87 - 0
packages/app/src/pages/invited.page.tsx

@@ -0,0 +1,87 @@
+import React from 'react';
+
+import { IUserHasId, IUser } from '@growi/core';
+import { NextPage, GetServerSideProps, GetServerSidePropsContext } from 'next';
+import { serverSideTranslations } from 'next-i18next/serverSideTranslations';
+import dynamic from 'next/dynamic';
+
+import { InvitedFormProps } from '~/components/InvitedForm';
+import { NoLoginLayout } from '~/components/Layout/NoLoginLayout';
+import { CrowiRequest } from '~/interfaces/crowi-request';
+
+import { useCsrfToken, useCurrentPathname, useCurrentUser } from '../stores/context';
+
+import {
+  CommonProps, getServerSideCommonProps, useCustomTitle, getNextI18NextConfig,
+} from './utils/commons';
+
+const InvitedForm = dynamic<InvitedFormProps>(() => import('~/components/InvitedForm').then(mod => mod.InvitedForm), { ssr: false });
+
+type Props = CommonProps & {
+  currentUser: IUser,
+  invitedFormUsername: string,
+  invitedFormName: string,
+}
+
+const InvitedPage: NextPage<Props> = (props: Props) => {
+
+  useCsrfToken(props.csrfToken);
+  useCurrentPathname(props.currentPathname);
+  useCurrentUser(props.currentUser);
+
+  const classNames: string[] = ['invited-page'];
+
+  return (
+    <NoLoginLayout title={useCustomTitle(props, 'GROWI')} className={classNames.join(' ')}>
+      <InvitedForm invitedFormUsername={props.invitedFormUsername} invitedFormName={props.invitedFormName} />
+    </NoLoginLayout>
+  );
+
+};
+
+async function injectServerConfigurations(context: GetServerSidePropsContext, props: Props): Promise<void> {
+  const req: CrowiRequest = context.req as CrowiRequest;
+  const { body: invitedForm } = req;
+
+  if (props.invitedFormUsername != null) {
+    props.invitedFormUsername = invitedForm.username;
+  }
+  if (props.invitedFormName != null) {
+    props.invitedFormName = invitedForm.name;
+  }
+}
+
+/**
+ * for Server Side Translations
+ * @param context
+ * @param props
+ * @param namespacesRequired
+ */
+async function injectNextI18NextConfigurations(context: GetServerSidePropsContext, props: Props, namespacesRequired?: string[] | undefined): Promise<void> {
+  const nextI18NextConfig = await getNextI18NextConfig(serverSideTranslations, context, namespacesRequired);
+  props._nextI18Next = nextI18NextConfig._nextI18Next;
+}
+
+export const getServerSideProps: GetServerSideProps = async(context: GetServerSidePropsContext) => {
+  const req = context.req as CrowiRequest<IUserHasId & any>;
+  const { user } = req;
+  const result = await getServerSideCommonProps(context);
+
+  // check for presence
+  // see: https://github.com/vercel/next.js/issues/19271#issuecomment-730006862
+  if (!('props' in result)) {
+    throw new Error('invalid getSSP result');
+  }
+  const props: Props = result.props as Props;
+
+  if (user != null) {
+    props.currentUser = user.toObject();
+  }
+
+  await injectServerConfigurations(context, props);
+  await injectNextI18NextConfigurations(context, props, ['translation']);
+
+  return { props };
+};
+
+export default InvitedPage;

+ 4 - 8
packages/app/src/pages/login.page.tsx

@@ -19,6 +19,8 @@ import {
   CommonProps, getServerSideCommonProps, useCustomTitle,
 } from './utils/commons';
 
+const LoginForm = dynamic(() => import('~/components/LoginForm'), { ssr: false });
+
 type Props = CommonProps & {
 
   pageWithMetaStr: string,
@@ -37,16 +39,10 @@ const LoginPage: NextPage<Props> = (props: Props) => {
 
   const classNames: string[] = ['login-page'];
 
-  const LoginForm = dynamic(() => import('~/components/LoginForm'), {
-    ssr: false,
-  });
-
   return (
     <NoLoginLayout title={useCustomTitle(props, 'GROWI')} className={classNames.join(' ')}>
-      <div className="col-md-12">
-        <LoginForm objOfIsExternalAuthEnableds={props.enabledStrategies} isLocalStrategySetup={true} isLdapStrategySetup={true}
-          isRegistrationEnabled={true} registrationWhiteList={props.registrationWhiteList} isPasswordResetEnabled={true} />
-      </div>
+      <LoginForm objOfIsExternalAuthEnableds={props.enabledStrategies} isLocalStrategySetup={true} isLdapStrategySetup={true}
+        isRegistrationEnabled={true} registrationWhiteList={props.registrationWhiteList} isPasswordResetEnabled={true} />
     </NoLoginLayout>
   );
 };

+ 1 - 1
packages/app/src/server/middlewares/login-required.js

@@ -27,7 +27,7 @@ module.exports = (crowi, isGuestAllowed = false, fallback = null) => {
         return res.redirect('/login/error/suspended');
       }
       if (req.user.status === User.STATUS_INVITED) {
-        return res.redirect('/login/invited');
+        return res.redirect('/invited');
       }
     }
 

+ 2 - 2
packages/app/src/server/routes/index.js

@@ -80,8 +80,8 @@ module.exports = function(crowi, app) {
 
   app.get('/login/error/:reason'      , applicationInstalled, login.error);
   app.get('/login'                    , applicationInstalled, login.preLogin, next.delegateToNext);
-  app.get('/login/invited'            , applicationInstalled, login.invited);
-  app.post('/login/activateInvited'   , applicationInstalled, loginFormValidator.inviteRules(), loginFormValidator.inviteValidation, csrfProtection, login.invited);
+  app.get('/invited'                  , applicationInstalled, next.delegateToNext);
+  app.post('/invited/activateInvited' , applicationInstalled, loginFormValidator.inviteRules(), loginFormValidator.inviteValidation, csrfProtection, login.invited);
   app.post('/login'                   , applicationInstalled, loginFormValidator.loginRules(), loginFormValidator.loginValidation, csrfProtection,  addActivity, loginPassport.loginWithLocal, loginPassport.loginWithLdap, loginPassport.loginFailure);
 
   app.post('/register'                , applicationInstalled, registerFormValidator.registerRules(), registerFormValidator.registerValidation, csrfProtection, addActivity, login.register);

+ 3 - 3
packages/app/test/integration/middlewares/login-required.test.js

@@ -47,7 +47,7 @@ describe('loginRequired', () => {
         userStatus  | expectedPath
         ${1}        | ${'/login/error/registered'}
         ${3}        | ${'/login/error/suspended'}
-        ${5}        | ${'/login/invited'}
+        ${5}        | ${'/invited'}
       `('redirect to \'$expectedPath\' when user.status is \'$userStatus\'', ({ userStatus, expectedPath }) => {
 
         req.user = {
@@ -122,7 +122,7 @@ describe('loginRequired', () => {
         userStatus  | expectedPath
         ${1}        | ${'/login/error/registered'}
         ${3}        | ${'/login/error/suspended'}
-        ${5}        | ${'/login/invited'}
+        ${5}        | ${'/invited'}
       `('redirect to \'$expectedPath\' when user.status is \'$userStatus\'', ({ userStatus, expectedPath }) => {
 
         req.user = {
@@ -248,7 +248,7 @@ describe('loginRequired', () => {
       userStatus  | expectedPath
       ${1}        | ${'/login/error/registered'}
       ${3}        | ${'/login/error/suspended'}
-      ${5}        | ${'/login/invited'}
+      ${5}        | ${'/invited'}
     `('redirect to \'$expectedPath\' when user.status is \'$userStatus\'', ({ userStatus, expectedPath }) => {
       req.user = {
         _id: 'user id',