mirror of
				https://github.com/cinnyapp/cinny.git
				synced 2025-11-04 06:20:28 +03:00 
			
		
		
		
	Signed-off-by: Ajay Bura <ajbura@gmail.com>
This commit is contained in:
		
							parent
							
								
									3d885ec262
								
							
						
					
					
						commit
						a83aecaa69
					
				
					 13 changed files with 850 additions and 708 deletions
				
			
		
							
								
								
									
										14
									
								
								config.json
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										14
									
								
								config.json
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,14 @@
 | 
			
		|||
{
 | 
			
		||||
  "defaultHomeserver": 5,
 | 
			
		||||
  "homeserverList": [
 | 
			
		||||
    "boba.best",
 | 
			
		||||
    "converser.eu",
 | 
			
		||||
    "envs.net",
 | 
			
		||||
    "halogen.city",
 | 
			
		||||
    "kde.org",
 | 
			
		||||
    "matrix.org",
 | 
			
		||||
    "mozilla.modular.im",
 | 
			
		||||
    "perthchat.org",
 | 
			
		||||
    "ru-matrix.org"
 | 
			
		||||
  ]
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										89
									
								
								package-lock.json
									
										
									
										generated
									
									
									
								
							
							
						
						
									
										89
									
								
								package-lock.json
									
										
									
										generated
									
									
									
								
							| 
						 | 
				
			
			@ -16,6 +16,7 @@
 | 
			
		|||
        "dateformat": "^4.5.1",
 | 
			
		||||
        "emojibase-data": "^6.2.0",
 | 
			
		||||
        "flux": "^4.0.1",
 | 
			
		||||
        "formik": "^2.2.9",
 | 
			
		||||
        "html-react-parser": "^1.2.7",
 | 
			
		||||
        "linkify-react": "^3.0.3",
 | 
			
		||||
        "matrix-js-sdk": "^12.4.1",
 | 
			
		||||
| 
						 | 
				
			
			@ -5558,6 +5559,14 @@
 | 
			
		|||
      "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==",
 | 
			
		||||
      "dev": true
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/deepmerge": {
 | 
			
		||||
      "version": "2.2.1",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-2.2.1.tgz",
 | 
			
		||||
      "integrity": "sha512-R9hc1Xa/NOBi9WRVUWg19rl1UB7Tt4kuPd+thNJgFZoxXsTz7ncaPaeIm+40oSGuP33DfMb4sZt1QIGiJzC4EA==",
 | 
			
		||||
      "engines": {
 | 
			
		||||
        "node": ">=0.10.0"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/default-gateway": {
 | 
			
		||||
      "version": "4.2.0",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/default-gateway/-/default-gateway-4.2.0.tgz",
 | 
			
		||||
| 
						 | 
				
			
			@ -7738,6 +7747,34 @@
 | 
			
		|||
        "node": ">=0.4.x"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/formik": {
 | 
			
		||||
      "version": "2.2.9",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/formik/-/formik-2.2.9.tgz",
 | 
			
		||||
      "integrity": "sha512-LQLcISMmf1r5at4/gyJigGn0gOwFbeEAlji+N9InZF6LIMXnFNkO42sCI8Jt84YZggpD4cPWObAZaxpEFtSzNA==",
 | 
			
		||||
      "funding": [
 | 
			
		||||
        {
 | 
			
		||||
          "type": "individual",
 | 
			
		||||
          "url": "https://opencollective.com/formik"
 | 
			
		||||
        }
 | 
			
		||||
      ],
 | 
			
		||||
      "dependencies": {
 | 
			
		||||
        "deepmerge": "^2.1.1",
 | 
			
		||||
        "hoist-non-react-statics": "^3.3.0",
 | 
			
		||||
        "lodash": "^4.17.21",
 | 
			
		||||
        "lodash-es": "^4.17.21",
 | 
			
		||||
        "react-fast-compare": "^2.0.1",
 | 
			
		||||
        "tiny-warning": "^1.0.2",
 | 
			
		||||
        "tslib": "^1.10.0"
 | 
			
		||||
      },
 | 
			
		||||
      "peerDependencies": {
 | 
			
		||||
        "react": ">=16.8.0"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/formik/node_modules/tslib": {
 | 
			
		||||
      "version": "1.14.1",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz",
 | 
			
		||||
      "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg=="
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/forwarded": {
 | 
			
		||||
      "version": "0.2.0",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
 | 
			
		||||
| 
						 | 
				
			
			@ -10297,8 +10334,12 @@
 | 
			
		|||
    "node_modules/lodash": {
 | 
			
		||||
      "version": "4.17.21",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
 | 
			
		||||
      "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
 | 
			
		||||
      "dev": true
 | 
			
		||||
      "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/lodash-es": {
 | 
			
		||||
      "version": "4.17.21",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.21.tgz",
 | 
			
		||||
      "integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw=="
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/lodash.clonedeep": {
 | 
			
		||||
      "version": "4.5.0",
 | 
			
		||||
| 
						 | 
				
			
			@ -14723,6 +14764,11 @@
 | 
			
		|||
        "react": "17.0.2"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/react-fast-compare": {
 | 
			
		||||
      "version": "2.0.4",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/react-fast-compare/-/react-fast-compare-2.0.4.tgz",
 | 
			
		||||
      "integrity": "sha512-suNP+J1VU1MWFKcyt7RtjiSWUjvidmQSlqu+eHslq+342xCbGTYmC0mEhPCOHxlW0CywylOC1u2DFAT+bv4dBw=="
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/react-google-recaptcha": {
 | 
			
		||||
      "version": "2.1.0",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/react-google-recaptcha/-/react-google-recaptcha-2.1.0.tgz",
 | 
			
		||||
| 
						 | 
				
			
			@ -23806,6 +23852,11 @@
 | 
			
		|||
      "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==",
 | 
			
		||||
      "dev": true
 | 
			
		||||
    },
 | 
			
		||||
    "deepmerge": {
 | 
			
		||||
      "version": "2.2.1",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-2.2.1.tgz",
 | 
			
		||||
      "integrity": "sha512-R9hc1Xa/NOBi9WRVUWg19rl1UB7Tt4kuPd+thNJgFZoxXsTz7ncaPaeIm+40oSGuP33DfMb4sZt1QIGiJzC4EA=="
 | 
			
		||||
    },
 | 
			
		||||
    "default-gateway": {
 | 
			
		||||
      "version": "4.2.0",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/default-gateway/-/default-gateway-4.2.0.tgz",
 | 
			
		||||
| 
						 | 
				
			
			@ -25526,6 +25577,27 @@
 | 
			
		|||
      "resolved": "https://registry.npmjs.org/format/-/format-0.2.2.tgz",
 | 
			
		||||
      "integrity": "sha1-1hcBB+nv3E7TDJ3DkBbflCtctYs="
 | 
			
		||||
    },
 | 
			
		||||
    "formik": {
 | 
			
		||||
      "version": "2.2.9",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/formik/-/formik-2.2.9.tgz",
 | 
			
		||||
      "integrity": "sha512-LQLcISMmf1r5at4/gyJigGn0gOwFbeEAlji+N9InZF6LIMXnFNkO42sCI8Jt84YZggpD4cPWObAZaxpEFtSzNA==",
 | 
			
		||||
      "requires": {
 | 
			
		||||
        "deepmerge": "^2.1.1",
 | 
			
		||||
        "hoist-non-react-statics": "^3.3.0",
 | 
			
		||||
        "lodash": "^4.17.21",
 | 
			
		||||
        "lodash-es": "^4.17.21",
 | 
			
		||||
        "react-fast-compare": "^2.0.1",
 | 
			
		||||
        "tiny-warning": "^1.0.2",
 | 
			
		||||
        "tslib": "^1.10.0"
 | 
			
		||||
      },
 | 
			
		||||
      "dependencies": {
 | 
			
		||||
        "tslib": {
 | 
			
		||||
          "version": "1.14.1",
 | 
			
		||||
          "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz",
 | 
			
		||||
          "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg=="
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "forwarded": {
 | 
			
		||||
      "version": "0.2.0",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
 | 
			
		||||
| 
						 | 
				
			
			@ -27531,8 +27603,12 @@
 | 
			
		|||
    "lodash": {
 | 
			
		||||
      "version": "4.17.21",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
 | 
			
		||||
      "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
 | 
			
		||||
      "dev": true
 | 
			
		||||
      "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="
 | 
			
		||||
    },
 | 
			
		||||
    "lodash-es": {
 | 
			
		||||
      "version": "4.17.21",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.21.tgz",
 | 
			
		||||
      "integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw=="
 | 
			
		||||
    },
 | 
			
		||||
    "lodash.clonedeep": {
 | 
			
		||||
      "version": "4.5.0",
 | 
			
		||||
| 
						 | 
				
			
			@ -30822,6 +30898,11 @@
 | 
			
		|||
        "scheduler": "^0.20.2"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "react-fast-compare": {
 | 
			
		||||
      "version": "2.0.4",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/react-fast-compare/-/react-fast-compare-2.0.4.tgz",
 | 
			
		||||
      "integrity": "sha512-suNP+J1VU1MWFKcyt7RtjiSWUjvidmQSlqu+eHslq+342xCbGTYmC0mEhPCOHxlW0CywylOC1u2DFAT+bv4dBw=="
 | 
			
		||||
    },
 | 
			
		||||
    "react-google-recaptcha": {
 | 
			
		||||
      "version": "2.1.0",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/react-google-recaptcha/-/react-google-recaptcha-2.1.0.tgz",
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -22,6 +22,7 @@
 | 
			
		|||
    "dateformat": "^4.5.1",
 | 
			
		||||
    "emojibase-data": "^6.2.0",
 | 
			
		||||
    "flux": "^4.0.1",
 | 
			
		||||
    "formik": "^2.2.9",
 | 
			
		||||
    "html-react-parser": "^1.2.7",
 | 
			
		||||
    "linkify-react": "^3.0.3",
 | 
			
		||||
    "matrix-js-sdk": "^12.4.1",
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -6,13 +6,14 @@ import Text from '../text/Text';
 | 
			
		|||
import RawIcon from '../system-icons/RawIcon';
 | 
			
		||||
import { blurOnBubbling } from './script';
 | 
			
		||||
 | 
			
		||||
function Button({
 | 
			
		||||
const Button = React.forwardRef(({
 | 
			
		||||
  id, className, variant, iconSrc,
 | 
			
		||||
  type, onClick, children, disabled,
 | 
			
		||||
}) {
 | 
			
		||||
}, ref) => {
 | 
			
		||||
  const iconClass = (iconSrc === null) ? '' : `btn-${variant}--icon`;
 | 
			
		||||
  return (
 | 
			
		||||
    <button
 | 
			
		||||
      ref={ref}
 | 
			
		||||
      id={id === '' ? undefined : id}
 | 
			
		||||
      className={`${className ? `${className} ` : ''}btn-${variant} ${iconClass} noselect`}
 | 
			
		||||
      onMouseUp={(e) => blurOnBubbling(e, `.btn-${variant}`)}
 | 
			
		||||
| 
						 | 
				
			
			@ -26,7 +27,7 @@ function Button({
 | 
			
		|||
      {typeof children !== 'string' && children }
 | 
			
		||||
    </button>
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
Button.defaultProps = {
 | 
			
		||||
  id: '',
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -5,7 +5,7 @@ import './Input.scss';
 | 
			
		|||
import TextareaAutosize from 'react-autosize-textarea';
 | 
			
		||||
 | 
			
		||||
function Input({
 | 
			
		||||
  id, label, value, placeholder,
 | 
			
		||||
  id, label, name, value, placeholder,
 | 
			
		||||
  required, type, onChange, forwardRef,
 | 
			
		||||
  resizable, minHeight, onResize, state,
 | 
			
		||||
  onKeyDown,
 | 
			
		||||
| 
						 | 
				
			
			@ -17,6 +17,7 @@ function Input({
 | 
			
		|||
        ? (
 | 
			
		||||
          <TextareaAutosize
 | 
			
		||||
            style={{ minHeight: `${minHeight}px` }}
 | 
			
		||||
            name={name}
 | 
			
		||||
            id={id}
 | 
			
		||||
            className={`input input--resizable${state !== 'normal' ? ` input--${state}` : ''}`}
 | 
			
		||||
            ref={forwardRef}
 | 
			
		||||
| 
						 | 
				
			
			@ -33,6 +34,7 @@ function Input({
 | 
			
		|||
          <input
 | 
			
		||||
            ref={forwardRef}
 | 
			
		||||
            id={id}
 | 
			
		||||
            name={name}
 | 
			
		||||
            className={`input ${state !== 'normal' ? ` input--${state}` : ''}`}
 | 
			
		||||
            type={type}
 | 
			
		||||
            placeholder={placeholder}
 | 
			
		||||
| 
						 | 
				
			
			@ -49,6 +51,7 @@ function Input({
 | 
			
		|||
 | 
			
		||||
Input.defaultProps = {
 | 
			
		||||
  id: null,
 | 
			
		||||
  name: '',
 | 
			
		||||
  label: '',
 | 
			
		||||
  value: '',
 | 
			
		||||
  placeholder: '',
 | 
			
		||||
| 
						 | 
				
			
			@ -65,6 +68,7 @@ Input.defaultProps = {
 | 
			
		|||
 | 
			
		||||
Input.propTypes = {
 | 
			
		||||
  id: PropTypes.string,
 | 
			
		||||
  name: PropTypes.string,
 | 
			
		||||
  label: PropTypes.string,
 | 
			
		||||
  value: PropTypes.string,
 | 
			
		||||
  placeholder: PropTypes.string,
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,107 +1,41 @@
 | 
			
		|||
import React, { useEffect, useState } from 'react';
 | 
			
		||||
import React from 'react';
 | 
			
		||||
import PropTypes from 'prop-types';
 | 
			
		||||
import './SSOButtons.scss';
 | 
			
		||||
 | 
			
		||||
import { createTemporaryClient, getLoginFlows, startSsoLogin } from '../../../client/action/auth';
 | 
			
		||||
import { createTemporaryClient, startSsoLogin } from '../../../client/action/auth';
 | 
			
		||||
 | 
			
		||||
import Text from '../../atoms/text/Text';
 | 
			
		||||
 | 
			
		||||
function SSOButtons({ homeserver }) {
 | 
			
		||||
  const [identityProviders, setIdentityProviders] = useState([]);
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    // Reset sso proviers to avoid displaying sso icons if the homeserver is not valid
 | 
			
		||||
    setIdentityProviders([]);
 | 
			
		||||
 | 
			
		||||
    // If the homeserver passed in is not a fully-qualified domain name, do not update.
 | 
			
		||||
    if (!homeserver.match('^[a-zA-Z0-9][a-zA-Z0-9-]{1,61}[a-zA-Z0-9](?:\\.[a-zA-Z]{2,})+$')) {
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // TODO Check that there is a Matrix server at homename before making requests.
 | 
			
		||||
    // This will prevent the CORS errors that happen when a user changes their homeserver.
 | 
			
		||||
    createTemporaryClient(homeserver).then((client) => {
 | 
			
		||||
      const providers = [];
 | 
			
		||||
      getLoginFlows(client).then((flows) => {
 | 
			
		||||
        if (flows.flows !== undefined) {
 | 
			
		||||
          const ssoFlows = flows.flows.filter((flow) => flow.type === 'm.login.sso' || flow.type === 'm.login.cas');
 | 
			
		||||
          ssoFlows.forEach((flow) => {
 | 
			
		||||
            if (flow.identity_providers !== undefined) {
 | 
			
		||||
              const type = flow.type.substring(8);
 | 
			
		||||
              flow.identity_providers.forEach((idp) => {
 | 
			
		||||
                const imageSrc = client.mxcUrlToHttp(idp.icon);
 | 
			
		||||
                providers.push({
 | 
			
		||||
                  homeserver, id: idp.id, name: idp.name, type, imageSrc,
 | 
			
		||||
                });
 | 
			
		||||
              });
 | 
			
		||||
            }
 | 
			
		||||
          });
 | 
			
		||||
        }
 | 
			
		||||
        setIdentityProviders(providers);
 | 
			
		||||
      }).catch(() => {});
 | 
			
		||||
    }).catch(() => {
 | 
			
		||||
      setIdentityProviders([]);
 | 
			
		||||
    });
 | 
			
		||||
  }, [homeserver]);
 | 
			
		||||
 | 
			
		||||
  if (identityProviders.length === 0) return <></>;
 | 
			
		||||
import Button from '../../atoms/button/Button';
 | 
			
		||||
 | 
			
		||||
function SSOButtons({ type, identityProviders, baseUrl }) {
 | 
			
		||||
  const tempClient = createTemporaryClient(baseUrl);
 | 
			
		||||
  function handleClick(id) {
 | 
			
		||||
    startSsoLogin(baseUrl, type, id);
 | 
			
		||||
  }
 | 
			
		||||
  return (
 | 
			
		||||
    <div className="sso-buttons">
 | 
			
		||||
      <div className="sso-buttons__divider">
 | 
			
		||||
        <Text>OR</Text>
 | 
			
		||||
      </div>
 | 
			
		||||
      <div className="sso-buttons__container">
 | 
			
		||||
        {identityProviders
 | 
			
		||||
          // Sort by alphabetical order
 | 
			
		||||
          .sort((idp, idp2) => {
 | 
			
		||||
            if (typeof idp.imageSrc !== 'string') return -1;
 | 
			
		||||
            return idp.name.toLowerCase() > idp2.name.toLowerCase() ? 1 : -1;
 | 
			
		||||
          })
 | 
			
		||||
          .map((idp) => (
 | 
			
		||||
            <SSOButton
 | 
			
		||||
              key={idp.id}
 | 
			
		||||
              homeserver={idp.homeserver}
 | 
			
		||||
              id={idp.id}
 | 
			
		||||
              name={idp.name}
 | 
			
		||||
              type={idp.type}
 | 
			
		||||
              imageSrc={idp.imageSrc}
 | 
			
		||||
            />
 | 
			
		||||
          ))}
 | 
			
		||||
      </div>
 | 
			
		||||
      {identityProviders
 | 
			
		||||
        .sort((idp, idp2) => {
 | 
			
		||||
          if (typeof idp.icon !== 'string') return -1;
 | 
			
		||||
          return idp.name.toLowerCase() > idp2.name.toLowerCase() ? 1 : -1;
 | 
			
		||||
        })
 | 
			
		||||
        .map((idp) => (
 | 
			
		||||
          idp.icon
 | 
			
		||||
            ? (
 | 
			
		||||
              <button key={idp.id} type="button" className="sso-btn" onClick={() => handleClick(idp.id)}>
 | 
			
		||||
                <img className="sso-btn__img" src={tempClient.mxcUrlToHttp(idp.icon, 36, 36, 'crop')} alt={idp.name} />
 | 
			
		||||
              </button>
 | 
			
		||||
            ) : <Button key={idp.id} className="sso-btn__text-only" onClick={() => handleClick(idp.id)}>{`Login with ${idp.name}`}</Button>
 | 
			
		||||
        ))}
 | 
			
		||||
    </div>
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function SSOButton({
 | 
			
		||||
  homeserver, id, name, type, imageSrc,
 | 
			
		||||
}) {
 | 
			
		||||
  const isImageAvail = !!imageSrc;
 | 
			
		||||
  function handleClick() {
 | 
			
		||||
    startSsoLogin(homeserver, type, id);
 | 
			
		||||
  }
 | 
			
		||||
  return (
 | 
			
		||||
    <button
 | 
			
		||||
      type="button"
 | 
			
		||||
      className={`sso-btn${!isImageAvail ? ' sso-btn__text-only' : ''}`}
 | 
			
		||||
      onClick={handleClick}
 | 
			
		||||
    >
 | 
			
		||||
      {isImageAvail && <img className="sso-btn__img" src={imageSrc} alt={name} />}
 | 
			
		||||
      {!isImageAvail && <Text>{`Login with ${name}`}</Text>}
 | 
			
		||||
    </button>
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
SSOButtons.propTypes = {
 | 
			
		||||
  homeserver: PropTypes.string.isRequired,
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
SSOButton.propTypes = {
 | 
			
		||||
  homeserver: PropTypes.string.isRequired,
 | 
			
		||||
  id: PropTypes.string.isRequired,
 | 
			
		||||
  name: PropTypes.string.isRequired,
 | 
			
		||||
  type: PropTypes.string.isRequired,
 | 
			
		||||
  imageSrc: PropTypes.string.isRequired,
 | 
			
		||||
  identityProviders: PropTypes.arrayOf(
 | 
			
		||||
    PropTypes.shape({}),
 | 
			
		||||
  ).isRequired,
 | 
			
		||||
  baseUrl: PropTypes.string.isRequired,
 | 
			
		||||
  type: PropTypes.oneOf(['sso', 'cas']).isRequired,
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export default SSOButtons;
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,22 +1,7 @@
 | 
			
		|||
.sso-buttons {
 | 
			
		||||
  &__divider {
 | 
			
		||||
    display: flex;
 | 
			
		||||
    align-items: center;
 | 
			
		||||
 | 
			
		||||
    &::before,
 | 
			
		||||
    &::after {
 | 
			
		||||
      flex: 1;
 | 
			
		||||
      content: '';
 | 
			
		||||
      margin: var(--sp-tight);
 | 
			
		||||
      border-bottom: 1px solid var(--bg-surface-border);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  &__container {
 | 
			
		||||
    margin-bottom: var(--sp-extra-loose);
 | 
			
		||||
    display: flex;
 | 
			
		||||
    justify-content: center;
 | 
			
		||||
    flex-wrap: wrap;  
 | 
			
		||||
  }
 | 
			
		||||
  display: flex;
 | 
			
		||||
  justify-content: center;
 | 
			
		||||
  flex-wrap: wrap;  
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.sso-btn {
 | 
			
		||||
| 
						 | 
				
			
			@ -31,11 +16,8 @@
 | 
			
		|||
    width: var(--av-small);
 | 
			
		||||
  }
 | 
			
		||||
  &__text-only {
 | 
			
		||||
    margin-top: var(--sp-normal);
 | 
			
		||||
    flex-basis: 100%;
 | 
			
		||||
    text-align: center;
 | 
			
		||||
 | 
			
		||||
    margin: var(--sp-tight) 0px;
 | 
			
		||||
    cursor: pointer;
 | 
			
		||||
    & .text {
 | 
			
		||||
      color: var(--tc-link);
 | 
			
		||||
    }
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -3,6 +3,7 @@ import PropTypes from 'prop-types';
 | 
			
		|||
import './Settings.scss';
 | 
			
		||||
 | 
			
		||||
import initMatrix from '../../../client/initMatrix';
 | 
			
		||||
import cons from '../../../client/state/cons';
 | 
			
		||||
import settings from '../../../client/state/settings';
 | 
			
		||||
import { toggleMarkdown } from '../../../client/action/settings';
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -104,7 +105,7 @@ function AboutSection() {
 | 
			
		|||
        <div>
 | 
			
		||||
          <Text variant="h2">
 | 
			
		||||
            Cinny
 | 
			
		||||
            <span className="text text-b3" style={{ margin: '0 var(--sp-extra-tight)' }}>v1.4.0</span>
 | 
			
		||||
            <span className="text text-b3" style={{ margin: '0 var(--sp-extra-tight)' }}>{`v${cons.version}`}</span>
 | 
			
		||||
          </Text>
 | 
			
		||||
          <Text>Yet another matrix client</Text>
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,11 +1,14 @@
 | 
			
		|||
import React, { useState, useRef } from 'react';
 | 
			
		||||
/* eslint-disable react/prop-types */
 | 
			
		||||
import React, { useState, useEffect, useRef } from 'react';
 | 
			
		||||
import PropTypes from 'prop-types';
 | 
			
		||||
import './Auth.scss';
 | 
			
		||||
import ReCAPTCHA from 'react-google-recaptcha';
 | 
			
		||||
import { Formik } from 'formik';
 | 
			
		||||
 | 
			
		||||
import { useLocation } from 'react-router-dom';
 | 
			
		||||
import * as auth from '../../../client/action/auth';
 | 
			
		||||
import cons from '../../../client/state/cons';
 | 
			
		||||
import { Debounce, getUrlPrams } from '../../../util/common';
 | 
			
		||||
import { getBaseUrl } from '../../../util/matrixUtil';
 | 
			
		||||
 | 
			
		||||
import Text from '../../atoms/text/Text';
 | 
			
		||||
import Button from '../../atoms/button/Button';
 | 
			
		||||
| 
						 | 
				
			
			@ -13,356 +16,552 @@ import IconButton from '../../atoms/button/IconButton';
 | 
			
		|||
import Input from '../../atoms/input/Input';
 | 
			
		||||
import Spinner from '../../atoms/spinner/Spinner';
 | 
			
		||||
import ScrollView from '../../atoms/scroll/ScrollView';
 | 
			
		||||
import Header, { TitleWrapper } from '../../atoms/header/Header';
 | 
			
		||||
import Avatar from '../../atoms/avatar/Avatar';
 | 
			
		||||
import ContextMenu, { MenuItem, MenuHeader } from '../../atoms/context-menu/ContextMenu';
 | 
			
		||||
 | 
			
		||||
import EyeIC from '../../../../public/res/ic/outlined/eye.svg';
 | 
			
		||||
import ChevronBottomIC from '../../../../public/res/ic/outlined/chevron-bottom.svg';
 | 
			
		||||
import CinnySvg from '../../../../public/res/svg/cinny.svg';
 | 
			
		||||
import SSOButtons from '../../molecules/sso-buttons/SSOButtons';
 | 
			
		||||
 | 
			
		||||
// This regex validates historical usernames, which don't satisfy today's username requirements.
 | 
			
		||||
// See https://matrix.org/docs/spec/appendices#id13 for more info.
 | 
			
		||||
const LOCALPART_LOGIN_REGEX = /.*/;
 | 
			
		||||
const LOCALPART_SIGNUP_REGEX = /^[a-z0-9_\-.=/]+$/;
 | 
			
		||||
const BAD_LOCALPART_ERROR = 'Username must contain only a-z, 0-9, ., _, =, -, and /.';
 | 
			
		||||
const BAD_LOCALPART_ERROR = 'Username can only contain characters a-z, 0-9, or \'=_-./\'';
 | 
			
		||||
const USER_ID_TOO_LONG_ERROR = 'Your user ID, including the hostname, can\'t be more than 255 characters long.';
 | 
			
		||||
 | 
			
		||||
const PASSWORD_REGEX = /.+/;
 | 
			
		||||
const PASSWORD_STRENGHT_REGEX = /^(?=.*\d)(?=.*[A-Z])(?=.*[a-z])(?=.*[^\w\d\s:])([^\s]){8,127}$/;
 | 
			
		||||
const BAD_PASSWORD_ERROR = 'Password must contain at least 1 number, 1 uppercase letter, 1 lowercase letter, 1 non-alphanumeric character. Passwords can range from 8-127 characters with no whitespaces.';
 | 
			
		||||
const BAD_PASSWORD_ERROR = 'Password must contain at least 1 lowercase, 1 uppercase, 1 number, 1 non-alphanumeric character, 8-127 characters with no space.';
 | 
			
		||||
const CONFIRM_PASSWORD_ERROR = 'Passwords don\'t match.';
 | 
			
		||||
 | 
			
		||||
const EMAIL_REGEX = /([a-z0-9]+[_a-z0-9.-][a-z0-9]+)@([a-z0-9-]+(?:.[a-z0-9-]+).[a-z]{2,4})/;
 | 
			
		||||
const EMAIL_REGEX = /^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$/i;
 | 
			
		||||
const BAD_EMAIL_ERROR = 'Invalid email address';
 | 
			
		||||
 | 
			
		||||
function isValidInput(value, regex) {
 | 
			
		||||
  if (typeof regex === 'string') return regex === value;
 | 
			
		||||
  return regex.test(value);
 | 
			
		||||
}
 | 
			
		||||
function renderErrorMessage(error) {
 | 
			
		||||
  const $error = document.getElementById('auth_error');
 | 
			
		||||
  $error.textContent = error;
 | 
			
		||||
  $error.style.display = 'block';
 | 
			
		||||
}
 | 
			
		||||
function showBadInputError($input, error, stopAutoFocus) {
 | 
			
		||||
  renderErrorMessage(error);
 | 
			
		||||
  if (!stopAutoFocus) $input.focus();
 | 
			
		||||
  const myInput = $input;
 | 
			
		||||
  myInput.style.border = '1px solid var(--bg-danger)';
 | 
			
		||||
  myInput.style.boxShadow = 'none';
 | 
			
		||||
  document.getElementById('auth_submit-btn').disabled = true;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function validateOnChange(targetInput, regex, error, stopAutoFocus) {
 | 
			
		||||
  if (!isValidInput(targetInput.value, regex) && targetInput.value) {
 | 
			
		||||
    showBadInputError(targetInput, error, stopAutoFocus);
 | 
			
		||||
    return false;
 | 
			
		||||
  }
 | 
			
		||||
  document.getElementById('auth_error').style.display = 'none';
 | 
			
		||||
  targetInput.style.removeProperty('border');
 | 
			
		||||
  targetInput.style.removeProperty('box-shadow');
 | 
			
		||||
  document.getElementById('auth_submit-btn').disabled = false;
 | 
			
		||||
  return true;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Normalizes a username into a standard format.
 | 
			
		||||
 *
 | 
			
		||||
 * Removes leading and trailing whitespaces and leading "@" symbols.
 | 
			
		||||
 * @param {string} rawUsername A raw-input username, which may include invalid characters.
 | 
			
		||||
 * @returns {string}
 | 
			
		||||
 */
 | 
			
		||||
function normalizeUsername(rawUsername) {
 | 
			
		||||
  const noLeadingAt = rawUsername.indexOf('@') === 0 ? rawUsername.substr(1) : rawUsername;
 | 
			
		||||
  return noLeadingAt.trim();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function Auth() {
 | 
			
		||||
  const [type, setType] = useState('login');
 | 
			
		||||
  const [process, changeProcess] = useState(null);
 | 
			
		||||
  const [homeserver, changeHomeserver] = useState('matrix.org');
 | 
			
		||||
let searchingHs = null;
 | 
			
		||||
function Homeserver({ onChange }) {
 | 
			
		||||
  const [hs, setHs] = useState(null);
 | 
			
		||||
  const [debounce] = useState(new Debounce());
 | 
			
		||||
  const [process, setProcess] = useState({ isLoading: true, message: 'Loading homeserver list...' });
 | 
			
		||||
  const hsRef = useRef();
 | 
			
		||||
 | 
			
		||||
  const usernameRef = useRef(null);
 | 
			
		||||
  const homeserverRef = useRef(null);
 | 
			
		||||
  const passwordRef = useRef(null);
 | 
			
		||||
  const confirmPasswordRef = useRef(null);
 | 
			
		||||
  const emailRef = useRef(null);
 | 
			
		||||
 | 
			
		||||
  const { search } = useLocation();
 | 
			
		||||
  const searchParams = new URLSearchParams(search);
 | 
			
		||||
  if (searchParams.has('loginToken')) {
 | 
			
		||||
    const loginToken = searchParams.get('loginToken');
 | 
			
		||||
    if (loginToken !== undefined) {
 | 
			
		||||
      if (localStorage.getItem(cons.secretKey.BASE_URL) !== undefined) {
 | 
			
		||||
        const baseUrl = localStorage.getItem(cons.secretKey.BASE_URL);
 | 
			
		||||
        auth.loginWithToken(baseUrl, loginToken)
 | 
			
		||||
          .then(() => {
 | 
			
		||||
            const { href } = window.location;
 | 
			
		||||
            window.location.replace(href.slice(0, href.indexOf('?')));
 | 
			
		||||
          })
 | 
			
		||||
          .catch((error) => {
 | 
			
		||||
            changeProcess(null);
 | 
			
		||||
            if (!error.contains('CORS request rejected')) {
 | 
			
		||||
              renderErrorMessage(error);
 | 
			
		||||
            }
 | 
			
		||||
          });
 | 
			
		||||
      }
 | 
			
		||||
  const setupHsConfig = async (servername) => {
 | 
			
		||||
    setProcess({ isLoading: true, message: 'Looking for homeserver...' });
 | 
			
		||||
    let baseUrl = null;
 | 
			
		||||
    try {
 | 
			
		||||
      baseUrl = await getBaseUrl(servername);
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
      baseUrl = e.message;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
    if (searchingHs !== servername) return;
 | 
			
		||||
    setProcess({ isLoading: true, message: `Connecting to ${baseUrl}...` });
 | 
			
		||||
    const tempClient = auth.createTemporaryClient(baseUrl);
 | 
			
		||||
 | 
			
		||||
  function register(recaptchaValue, terms, verified) {
 | 
			
		||||
    auth.register(
 | 
			
		||||
      usernameRef.current.value,
 | 
			
		||||
      homeserverRef.current.value,
 | 
			
		||||
      passwordRef.current.value,
 | 
			
		||||
      emailRef.current.value,
 | 
			
		||||
      recaptchaValue,
 | 
			
		||||
      terms,
 | 
			
		||||
      verified,
 | 
			
		||||
    ).then((res) => {
 | 
			
		||||
      document.getElementById('auth_submit-btn').disabled = false;
 | 
			
		||||
      if (res.type === 'recaptcha') {
 | 
			
		||||
        changeProcess({ type: res.type, sitekey: res.public_key });
 | 
			
		||||
        return;
 | 
			
		||||
      }
 | 
			
		||||
      if (res.type === 'terms') {
 | 
			
		||||
        changeProcess({ type: res.type, en: res.en });
 | 
			
		||||
      }
 | 
			
		||||
      if (res.type === 'email') {
 | 
			
		||||
        changeProcess({ type: res.type });
 | 
			
		||||
      }
 | 
			
		||||
      if (res.type === 'done') {
 | 
			
		||||
        window.location.replace('/');
 | 
			
		||||
      }
 | 
			
		||||
    }).catch((error) => {
 | 
			
		||||
      changeProcess(null);
 | 
			
		||||
      renderErrorMessage(error);
 | 
			
		||||
      document.getElementById('auth_submit-btn').disabled = false;
 | 
			
		||||
    });
 | 
			
		||||
    if (terms) {
 | 
			
		||||
      changeProcess({ type: 'loading', message: 'Sending email verification link...' });
 | 
			
		||||
    } else changeProcess({ type: 'loading', message: 'Registration in progress...' });
 | 
			
		||||
  }
 | 
			
		||||
    Promise.allSettled([tempClient.loginFlows(), tempClient.register()])
 | 
			
		||||
      .then((values) => {
 | 
			
		||||
        const loginFlow = values[0].status === 'fulfilled' ? values[0]?.value : undefined;
 | 
			
		||||
        const registerFlow = values[1].status === 'rejected' ? values[1]?.reason?.data : undefined;
 | 
			
		||||
        if (loginFlow === undefined || registerFlow === undefined) throw new Error();
 | 
			
		||||
 | 
			
		||||
  function handleLogin(e) {
 | 
			
		||||
    e.preventDefault();
 | 
			
		||||
    document.getElementById('auth_submit-btn').disabled = true;
 | 
			
		||||
    document.getElementById('auth_error').style.display = 'none';
 | 
			
		||||
 | 
			
		||||
    /** @type {string} */
 | 
			
		||||
    const rawUsername = usernameRef.current.value;
 | 
			
		||||
    /** @type {string} */
 | 
			
		||||
    const normalizedUsername = normalizeUsername(rawUsername);
 | 
			
		||||
 | 
			
		||||
    auth.login(normalizedUsername, homeserverRef.current.value, passwordRef.current.value)
 | 
			
		||||
      .then(() => {
 | 
			
		||||
        document.getElementById('auth_submit-btn').disabled = false;
 | 
			
		||||
        window.location.replace('/');
 | 
			
		||||
      })
 | 
			
		||||
      .catch((error) => {
 | 
			
		||||
        changeProcess(null);
 | 
			
		||||
        renderErrorMessage(error);
 | 
			
		||||
        document.getElementById('auth_submit-btn').disabled = false;
 | 
			
		||||
        if (searchingHs !== servername) return;
 | 
			
		||||
        onChange({ baseUrl, login: loginFlow, register: registerFlow });
 | 
			
		||||
        setProcess({ isLoading: false });
 | 
			
		||||
      }).catch(() => {
 | 
			
		||||
        if (searchingHs !== servername) return;
 | 
			
		||||
        onChange(null);
 | 
			
		||||
        setProcess({ isLoading: false, error: 'Unable to connect. Please check your input.' });
 | 
			
		||||
      });
 | 
			
		||||
    changeProcess({ type: 'loading', message: 'Login in progress...' });
 | 
			
		||||
  }
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  function handleRegister(e) {
 | 
			
		||||
    e.preventDefault();
 | 
			
		||||
    document.getElementById('auth_submit-btn').disabled = true;
 | 
			
		||||
    document.getElementById('auth_error').style.display = 'none';
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    onChange(null);
 | 
			
		||||
    if (hs === null || hs?.selected.trim() === '') return;
 | 
			
		||||
    searchingHs = hs.selected;
 | 
			
		||||
    setupHsConfig(hs.selected);
 | 
			
		||||
  }, [hs]);
 | 
			
		||||
 | 
			
		||||
    if (!isValidInput(usernameRef.current.value, LOCALPART_SIGNUP_REGEX)) {
 | 
			
		||||
      showBadInputError(usernameRef.current, BAD_LOCALPART_ERROR);
 | 
			
		||||
      return;
 | 
			
		||||
  useEffect(async () => {
 | 
			
		||||
    const configFileUrl = `${window.location.href}/config.json`;
 | 
			
		||||
    try {
 | 
			
		||||
      const result = await (await fetch(configFileUrl, { method: 'GET' })).json();
 | 
			
		||||
      const selectedHs = result?.defaultHomeserver;
 | 
			
		||||
      const hsList = result?.homeserverList;
 | 
			
		||||
      if (!hsList?.length > 0 || selectedHs < 0 || selectedHs >= hsList?.length) {
 | 
			
		||||
        throw new Error();
 | 
			
		||||
      }
 | 
			
		||||
      setHs({ selected: hsList[selectedHs], list: hsList });
 | 
			
		||||
    } catch {
 | 
			
		||||
      setHs({ selected: 'matrix.org', list: ['matrix.org'] });
 | 
			
		||||
    }
 | 
			
		||||
    if (!isValidInput(passwordRef.current.value, PASSWORD_STRENGHT_REGEX)) {
 | 
			
		||||
      showBadInputError(passwordRef.current, BAD_PASSWORD_ERROR);
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
    if (passwordRef.current.value !== confirmPasswordRef.current.value) {
 | 
			
		||||
      showBadInputError(confirmPasswordRef.current, CONFIRM_PASSWORD_ERROR);
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
    if (!isValidInput(emailRef.current.value, EMAIL_REGEX)) {
 | 
			
		||||
      showBadInputError(emailRef.current, BAD_EMAIL_ERROR);
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
    if (`@${usernameRef.current.value}:${homeserverRef.current.value}`.length > 255) {
 | 
			
		||||
      showBadInputError(usernameRef.current, USER_ID_TOO_LONG_ERROR);
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
    register();
 | 
			
		||||
  }
 | 
			
		||||
  }, []);
 | 
			
		||||
 | 
			
		||||
  const handleHsInput = (e) => {
 | 
			
		||||
    const { value } = e.target;
 | 
			
		||||
    setProcess({ isLoading: false });
 | 
			
		||||
    debounce._(async () => {
 | 
			
		||||
      setHs({ selected: value, list: hs.list });
 | 
			
		||||
    }, 700)();
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  const handleAuth = (type === 'login') ? handleLogin : handleRegister;
 | 
			
		||||
  return (
 | 
			
		||||
    <>
 | 
			
		||||
      {process?.type === 'loading' && <LoadingScreen message={process.message} />}
 | 
			
		||||
      {process?.type === 'recaptcha' && <Recaptcha message="Please check the box below to proceed." sitekey={process.sitekey} onChange={(v) => { if (typeof v === 'string') register(v); }} />}
 | 
			
		||||
      {process?.type === 'terms' && <Terms url={process.en.url} onSubmit={register} />}
 | 
			
		||||
      {process?.type === 'email' && (
 | 
			
		||||
        <ProcessWrapper>
 | 
			
		||||
          <div style={{ margin: 'var(--sp-normal)', maxWidth: '450px' }}>
 | 
			
		||||
            <Text variant="h2">Verify email</Text>
 | 
			
		||||
            <div style={{ margin: 'var(--sp-normal) 0' }}>
 | 
			
		||||
              <Text variant="b1">
 | 
			
		||||
                Please check your email
 | 
			
		||||
                {' '}
 | 
			
		||||
                <b>{`(${emailRef.current.value})`}</b>
 | 
			
		||||
                {' '}
 | 
			
		||||
                and validate before continuing further.
 | 
			
		||||
              </Text>
 | 
			
		||||
            </div>
 | 
			
		||||
            <Button variant="primary" onClick={() => register(undefined, undefined, true)}>Continue</Button>
 | 
			
		||||
          </div>
 | 
			
		||||
        </ProcessWrapper>
 | 
			
		||||
      )}
 | 
			
		||||
      <StaticWrapper>
 | 
			
		||||
        <div className="auth-form__wrapper flex-v--center">
 | 
			
		||||
          <form onSubmit={handleAuth} className="auth-form">
 | 
			
		||||
            <Text variant="h2">{ type === 'login' ? 'Login' : 'Register' }</Text>
 | 
			
		||||
            <div className="username__wrapper">
 | 
			
		||||
              <Input
 | 
			
		||||
                forwardRef={usernameRef}
 | 
			
		||||
                onChange={(e) => (type === 'login'
 | 
			
		||||
                  ? validateOnChange(e.target, LOCALPART_LOGIN_REGEX, BAD_LOCALPART_ERROR)
 | 
			
		||||
                  : validateOnChange(e.target, LOCALPART_SIGNUP_REGEX, BAD_LOCALPART_ERROR))}
 | 
			
		||||
                id="auth_username"
 | 
			
		||||
                label="Username"
 | 
			
		||||
                required
 | 
			
		||||
              />
 | 
			
		||||
              <Input
 | 
			
		||||
                forwardRef={homeserverRef}
 | 
			
		||||
                onChange={(e) => changeHomeserver(e.target.value)}
 | 
			
		||||
                id="auth_homeserver"
 | 
			
		||||
                placeholder="Homeserver"
 | 
			
		||||
                value="matrix.org"
 | 
			
		||||
                required
 | 
			
		||||
              />
 | 
			
		||||
            </div>
 | 
			
		||||
            <div className="password__wrapper">
 | 
			
		||||
              <Input
 | 
			
		||||
                forwardRef={passwordRef}
 | 
			
		||||
                onChange={(e) => {
 | 
			
		||||
                  const isValidPass = validateOnChange(e.target, ((type === 'login') ? PASSWORD_REGEX : PASSWORD_STRENGHT_REGEX), BAD_PASSWORD_ERROR);
 | 
			
		||||
                  if (type === 'register' && isValidPass) {
 | 
			
		||||
                    validateOnChange(
 | 
			
		||||
                      confirmPasswordRef.current, passwordRef.current.value,
 | 
			
		||||
                      CONFIRM_PASSWORD_ERROR, true,
 | 
			
		||||
                    );
 | 
			
		||||
                  }
 | 
			
		||||
                }}
 | 
			
		||||
                id="auth_password"
 | 
			
		||||
                type="password"
 | 
			
		||||
                label="Password"
 | 
			
		||||
                required
 | 
			
		||||
              />
 | 
			
		||||
              <IconButton
 | 
			
		||||
                onClick={() => {
 | 
			
		||||
                  if (passwordRef.current.type === 'password') {
 | 
			
		||||
                    passwordRef.current.type = 'text';
 | 
			
		||||
                  } else passwordRef.current.type = 'password';
 | 
			
		||||
                }}
 | 
			
		||||
                size="extra-small"
 | 
			
		||||
                src={EyeIC}
 | 
			
		||||
              />
 | 
			
		||||
            </div>
 | 
			
		||||
            {type === 'register' && (
 | 
			
		||||
              <>
 | 
			
		||||
                <div className="password__wrapper">
 | 
			
		||||
                  <Input
 | 
			
		||||
                    forwardRef={confirmPasswordRef}
 | 
			
		||||
                    onChange={(e) => {
 | 
			
		||||
                      validateOnChange(e.target, passwordRef.current.value, CONFIRM_PASSWORD_ERROR);
 | 
			
		||||
                    }}
 | 
			
		||||
                    id="auth_confirmPassword"
 | 
			
		||||
                    type="password"
 | 
			
		||||
                    label="Confirm password"
 | 
			
		||||
                    required
 | 
			
		||||
                  />
 | 
			
		||||
                  <IconButton
 | 
			
		||||
      <div className="homeserver-form">
 | 
			
		||||
        <Input name="homeserver" onChange={handleHsInput} value={hs?.selected} forwardRef={hsRef} label="Homeserver" />
 | 
			
		||||
        <ContextMenu
 | 
			
		||||
          placement="right"
 | 
			
		||||
          content={(hideMenu) => (
 | 
			
		||||
            <>
 | 
			
		||||
              <MenuHeader>Homeserver list</MenuHeader>
 | 
			
		||||
              {
 | 
			
		||||
                hs?.list.map((hsName) => (
 | 
			
		||||
                  <MenuItem
 | 
			
		||||
                    key={hsName}
 | 
			
		||||
                    onClick={() => {
 | 
			
		||||
                      if (confirmPasswordRef.current.type === 'password') {
 | 
			
		||||
                        confirmPasswordRef.current.type = 'text';
 | 
			
		||||
                      } else confirmPasswordRef.current.type = 'password';
 | 
			
		||||
                      hideMenu();
 | 
			
		||||
                      hsRef.current.value = hsName;
 | 
			
		||||
                      setHs({ selected: hsName, list: hs.list });
 | 
			
		||||
                    }}
 | 
			
		||||
                    size="extra-small"
 | 
			
		||||
                    src={EyeIC}
 | 
			
		||||
                  />
 | 
			
		||||
                </div>
 | 
			
		||||
                <Input
 | 
			
		||||
                  forwardRef={emailRef}
 | 
			
		||||
                  onChange={(e) => validateOnChange(e.target, EMAIL_REGEX, BAD_EMAIL_ERROR)}
 | 
			
		||||
                  id="auth_email"
 | 
			
		||||
                  type="email"
 | 
			
		||||
                  label="Email"
 | 
			
		||||
                  required
 | 
			
		||||
                />
 | 
			
		||||
              </>
 | 
			
		||||
            )}
 | 
			
		||||
            <div className="submit-btn__wrapper flex--end">
 | 
			
		||||
              <Text id="auth_error" className="error-message" variant="b3">Error</Text>
 | 
			
		||||
              <Button
 | 
			
		||||
                id="auth_submit-btn"
 | 
			
		||||
                variant="primary"
 | 
			
		||||
                type="submit"
 | 
			
		||||
              >
 | 
			
		||||
                {type === 'login' ? 'Login' : 'Register' }
 | 
			
		||||
              </Button>
 | 
			
		||||
            </div>
 | 
			
		||||
            {type === 'login' && (
 | 
			
		||||
              <SSOButtons homeserver={homeserver} />
 | 
			
		||||
            )}
 | 
			
		||||
          </form>
 | 
			
		||||
                  >
 | 
			
		||||
                    {hsName}
 | 
			
		||||
                  </MenuItem>
 | 
			
		||||
                ))
 | 
			
		||||
              }
 | 
			
		||||
            </>
 | 
			
		||||
          )}
 | 
			
		||||
          render={(toggleMenu) => <IconButton onClick={toggleMenu} src={ChevronBottomIC} />}
 | 
			
		||||
        />
 | 
			
		||||
      </div>
 | 
			
		||||
      {process.error !== undefined && <Text className="homeserver-form__error" variant="b3">{process.error}</Text>}
 | 
			
		||||
      {process.isLoading && (
 | 
			
		||||
        <div className="homeserver-form__status flex--center">
 | 
			
		||||
          <Spinner size="small" />
 | 
			
		||||
          <Text variant="b2">{process.message}</Text>
 | 
			
		||||
        </div>
 | 
			
		||||
      )}
 | 
			
		||||
    </>
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
Homeserver.propTypes = {
 | 
			
		||||
  onChange: PropTypes.func.isRequired,
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
        <div style={{ flexDirection: 'column' }} className="flex--center">
 | 
			
		||||
          <Text variant="b2">
 | 
			
		||||
            {`${(type === 'login' ? 'Don\'t have' : 'Already have')} an account?`}
 | 
			
		||||
            <button
 | 
			
		||||
              type="button"
 | 
			
		||||
              style={{ color: 'var(--tc-link)', cursor: 'pointer', margin: '0 var(--sp-ultra-tight)' }}
 | 
			
		||||
              onClick={() => {
 | 
			
		||||
                if (type === 'login') setType('register');
 | 
			
		||||
                else setType('login');
 | 
			
		||||
              }}
 | 
			
		||||
            >
 | 
			
		||||
              { type === 'login' ? ' Register' : ' Login' }
 | 
			
		||||
            </button>
 | 
			
		||||
          </Text>
 | 
			
		||||
          <span style={{ marginTop: 'var(--sp-extra-tight)' }}>
 | 
			
		||||
            <Text variant="b3">v1.4.0</Text>
 | 
			
		||||
          </span>
 | 
			
		||||
        </div>
 | 
			
		||||
      </StaticWrapper>
 | 
			
		||||
function Login({ loginFlow, baseUrl }) {
 | 
			
		||||
  const [typeIndex, setTypeIndex] = useState(0);
 | 
			
		||||
  const loginTypes = ['Username', 'Email'];
 | 
			
		||||
  const isPassword = loginFlow?.filter((flow) => flow.type === 'm.login.password')[0];
 | 
			
		||||
  const ssoProviders = loginFlow?.filter((flow) => flow.type.match(/^m.login.(sso|cas)$/))[0];
 | 
			
		||||
 | 
			
		||||
  const initialValues = {
 | 
			
		||||
    username: '', password: '', email: '', other: '',
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  const validator = (values) => {
 | 
			
		||||
    const errors = {};
 | 
			
		||||
    if (typeIndex === 0 && values.username.length > 0 && values.username.indexOf(':') > -1) {
 | 
			
		||||
      errors.username = 'Username must contain local-part only';
 | 
			
		||||
    }
 | 
			
		||||
    if (typeIndex === 1 && values.email.length > 0 && !isValidInput(values.email, EMAIL_REGEX)) {
 | 
			
		||||
      errors.email = BAD_EMAIL_ERROR;
 | 
			
		||||
    }
 | 
			
		||||
    return errors;
 | 
			
		||||
  };
 | 
			
		||||
  const submitter = (values, actions) => auth.login(
 | 
			
		||||
    baseUrl,
 | 
			
		||||
    typeIndex === 0 ? normalizeUsername(values.username) : undefined,
 | 
			
		||||
    typeIndex === 1 ? values.email : undefined,
 | 
			
		||||
    values.password,
 | 
			
		||||
  ).then(() => {
 | 
			
		||||
    actions.setSubmitting(true);
 | 
			
		||||
    window.location.reload();
 | 
			
		||||
  }).catch((error) => {
 | 
			
		||||
    let msg = error.message;
 | 
			
		||||
    if (msg === 'Unknown message') msg = 'Please check your credentials';
 | 
			
		||||
    actions.setErrors({
 | 
			
		||||
      password: msg === 'Invalid password' ? msg : undefined,
 | 
			
		||||
      other: msg !== 'Invalid password' ? msg : undefined,
 | 
			
		||||
    });
 | 
			
		||||
    actions.setSubmitting(false);
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <>
 | 
			
		||||
      <div className="auth-form__heading">
 | 
			
		||||
        <Text variant="h2">Login</Text>
 | 
			
		||||
        {isPassword && (
 | 
			
		||||
          <ContextMenu
 | 
			
		||||
            placement="right"
 | 
			
		||||
            content={(hideMenu) => (
 | 
			
		||||
              loginTypes.map((type, index) => (
 | 
			
		||||
                <MenuItem
 | 
			
		||||
                  key={type}
 | 
			
		||||
                  onClick={() => {
 | 
			
		||||
                    hideMenu();
 | 
			
		||||
                    setTypeIndex(index);
 | 
			
		||||
                  }}
 | 
			
		||||
                >
 | 
			
		||||
                  {type}
 | 
			
		||||
                </MenuItem>
 | 
			
		||||
              ))
 | 
			
		||||
            )}
 | 
			
		||||
            render={(toggleMenu) => (
 | 
			
		||||
              <Button onClick={toggleMenu} iconSrc={ChevronBottomIC}>
 | 
			
		||||
                {loginTypes[typeIndex]}
 | 
			
		||||
              </Button>
 | 
			
		||||
            )}
 | 
			
		||||
          />
 | 
			
		||||
        )}
 | 
			
		||||
      </div>
 | 
			
		||||
      {isPassword && (
 | 
			
		||||
        <Formik
 | 
			
		||||
          initialValues={initialValues}
 | 
			
		||||
          onSubmit={submitter}
 | 
			
		||||
          validate={validator}
 | 
			
		||||
        >
 | 
			
		||||
          {({
 | 
			
		||||
            values, errors, handleChange, handleSubmit, isSubmitting,
 | 
			
		||||
          }) => (
 | 
			
		||||
            <>
 | 
			
		||||
              {isSubmitting && <LoadingScreen message="Login in progress..." />}
 | 
			
		||||
              <form className="auth-form" onSubmit={handleSubmit}>
 | 
			
		||||
                {typeIndex === 0 && <Input values={values.username} name="username" onChange={handleChange} label="Username" type="username" required />}
 | 
			
		||||
                {errors.username && <Text className="auth-form__error" variant="b3">{errors.username}</Text>}
 | 
			
		||||
                {typeIndex === 1 && <Input values={values.email} name="email" onChange={handleChange} label="Email" type="email" required />}
 | 
			
		||||
                {errors.email && <Text className="auth-form__error" variant="b3">{errors.email}</Text>}
 | 
			
		||||
                <Input values={values.password} name="password" onChange={handleChange} label="Password" type="password" required />
 | 
			
		||||
                {errors.password && <Text className="auth-form__error" variant="b3">{errors.password}</Text>}
 | 
			
		||||
                {errors.other && <Text className="auth-form__error" variant="b3">{errors.other}</Text>}
 | 
			
		||||
                <div className="auth-form__btns">
 | 
			
		||||
                  <Button variant="primary" type="submit" disabled={isSubmitting}>Login</Button>
 | 
			
		||||
                </div>
 | 
			
		||||
              </form>
 | 
			
		||||
            </>
 | 
			
		||||
          )}
 | 
			
		||||
        </Formik>
 | 
			
		||||
      )}
 | 
			
		||||
      {ssoProviders && isPassword && <Text className="sso__divider">OR</Text>}
 | 
			
		||||
      {ssoProviders && (
 | 
			
		||||
        <SSOButtons
 | 
			
		||||
          type={ssoProviders.type.match(/^m.login.(sso|cas)$/)[1]}
 | 
			
		||||
          identityProviders={ssoProviders.identity_providers}
 | 
			
		||||
          baseUrl={baseUrl}
 | 
			
		||||
        />
 | 
			
		||||
      )}
 | 
			
		||||
    </>
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
Login.propTypes = {
 | 
			
		||||
  loginFlow: PropTypes.arrayOf(
 | 
			
		||||
    PropTypes.shape({}),
 | 
			
		||||
  ).isRequired,
 | 
			
		||||
  baseUrl: PropTypes.string.isRequired,
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
let sid;
 | 
			
		||||
let clientSecret;
 | 
			
		||||
function Register({ registerInfo, loginFlow, baseUrl }) {
 | 
			
		||||
  const [process, setProcess] = useState({});
 | 
			
		||||
  const formRef = useRef();
 | 
			
		||||
 | 
			
		||||
  const ssoProviders = loginFlow?.filter((flow) => flow.type.match(/^m.login.(sso|cas)$/))[0];
 | 
			
		||||
  const isDisabled = registerInfo.errcode !== undefined;
 | 
			
		||||
  const { flows, params, session } = registerInfo;
 | 
			
		||||
 | 
			
		||||
  let isEmail = false;
 | 
			
		||||
  let isEmailRequired = true;
 | 
			
		||||
  let isRecaptcha = false;
 | 
			
		||||
  let isTerms = false;
 | 
			
		||||
  let isDummy = false;
 | 
			
		||||
 | 
			
		||||
  flows?.forEach((flow) => {
 | 
			
		||||
    if (isEmailRequired && flow.stages.indexOf('m.login.email.identity') === -1) isEmailRequired = false;
 | 
			
		||||
    if (!isEmail) isEmail = flow.stages.indexOf('m.login.email.identity') > -1;
 | 
			
		||||
    if (!isRecaptcha) isRecaptcha = flow.stages.indexOf('m.login.recaptcha') > -1;
 | 
			
		||||
    if (!isTerms) isTerms = flow.stages.indexOf('m.login.terms') > -1;
 | 
			
		||||
    if (!isDummy) isDummy = flow.stages.indexOf('m.login.dummy') > -1;
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  const initialValues = {
 | 
			
		||||
    username: '', password: '', confirmPassword: '', email: '', other: '',
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  const validator = (values) => {
 | 
			
		||||
    const errors = {};
 | 
			
		||||
    if (values.username.list > 255) errors.username = USER_ID_TOO_LONG_ERROR;
 | 
			
		||||
    if (values.username.length > 0 && !isValidInput(values.username, LOCALPART_SIGNUP_REGEX)) {
 | 
			
		||||
      errors.username = BAD_LOCALPART_ERROR;
 | 
			
		||||
    }
 | 
			
		||||
    if (values.password.length > 0 && !isValidInput(values.password, PASSWORD_STRENGHT_REGEX)) {
 | 
			
		||||
      errors.password = BAD_PASSWORD_ERROR;
 | 
			
		||||
    }
 | 
			
		||||
    if (values.confirmPassword.length > 0
 | 
			
		||||
      && !isValidInput(values.confirmPassword, values.password)) {
 | 
			
		||||
      errors.confirmPassword = CONFIRM_PASSWORD_ERROR;
 | 
			
		||||
    }
 | 
			
		||||
    if (values.email.length > 0 && !isValidInput(values.email, EMAIL_REGEX)) {
 | 
			
		||||
      errors.email = BAD_EMAIL_ERROR;
 | 
			
		||||
    }
 | 
			
		||||
    return errors;
 | 
			
		||||
  };
 | 
			
		||||
  const submitter = (values, actions) => {
 | 
			
		||||
    const tempClient = auth.createTemporaryClient(baseUrl);
 | 
			
		||||
    clientSecret = tempClient.generateClientSecret();
 | 
			
		||||
    return tempClient.isUsernameAvailable(values.username)
 | 
			
		||||
      .then(async (isAvail) => {
 | 
			
		||||
        if (!isAvail) {
 | 
			
		||||
          actions.setErrors({ username: 'Username is already taken' });
 | 
			
		||||
          actions.setSubmitting(false);
 | 
			
		||||
        }
 | 
			
		||||
        if (isEmail && values.email.length > 0) {
 | 
			
		||||
          const result = await auth.verifyEmail(baseUrl, values.email, clientSecret, 1);
 | 
			
		||||
          if (result.errcode) {
 | 
			
		||||
            if (result.errcode === 'M_THREEPID_IN_USE') actions.setErrors({ email: result.error });
 | 
			
		||||
            else actions.setErrors({ others: result.error || result.message });
 | 
			
		||||
            actions.setSubmitting(false);
 | 
			
		||||
            return;
 | 
			
		||||
          }
 | 
			
		||||
          sid = result.sid;
 | 
			
		||||
        }
 | 
			
		||||
        setProcess({ type: 'processing', message: 'Registration in progress....' });
 | 
			
		||||
        actions.setSubmitting(false);
 | 
			
		||||
      }).catch((err) => {
 | 
			
		||||
        const msg = err.message || err.error;
 | 
			
		||||
        if (['M_USER_IN_USE', 'M_INVALID_USERNAME', 'M_EXCLUSIVE'].indexOf(err.errcode) > 0) {
 | 
			
		||||
          actions.setErrors({ username: err.errCode === 'M_USER_IN_USE' ? 'Username is already taken' : msg });
 | 
			
		||||
        } else if (msg) actions.setErrors({ other: msg });
 | 
			
		||||
 | 
			
		||||
        actions.setSubmitting(false);
 | 
			
		||||
      });
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  const refreshWindow = () => window.location.reload();
 | 
			
		||||
 | 
			
		||||
  const getInputs = () => {
 | 
			
		||||
    const f = formRef.current;
 | 
			
		||||
    return [f.username.value, f.password.value, f?.email?.value];
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    if (process.type !== 'processing') return;
 | 
			
		||||
    const asyncProcess = async () => {
 | 
			
		||||
      const [username, password, email] = getInputs();
 | 
			
		||||
      const d = await auth.completeRegisterStage(baseUrl, username, password, { session });
 | 
			
		||||
 | 
			
		||||
      if (isRecaptcha && !d.completed.includes('m.login.recaptcha')) {
 | 
			
		||||
        const sitekey = params['m.login.recaptcha'].public_key;
 | 
			
		||||
        setProcess({ type: 'm.login.recaptcha', sitekey });
 | 
			
		||||
        return;
 | 
			
		||||
      }
 | 
			
		||||
      if (isTerms && !d.completed.includes('m.login.terms')) {
 | 
			
		||||
        const pp = params['m.login.terms'].policies.privacy_policy;
 | 
			
		||||
        const url = pp?.en.url || pp[Object.keys(pp)[0]].url;
 | 
			
		||||
        setProcess({ type: 'm.login.terms', url });
 | 
			
		||||
        return;
 | 
			
		||||
      }
 | 
			
		||||
      if (isEmail && email.length > 0) {
 | 
			
		||||
        setProcess({ type: 'm.login.email.identity', email });
 | 
			
		||||
        return;
 | 
			
		||||
      }
 | 
			
		||||
      if (isDummy) {
 | 
			
		||||
        const data = await auth.completeRegisterStage(baseUrl, username, password, {
 | 
			
		||||
          type: 'm.login.dummy',
 | 
			
		||||
          session,
 | 
			
		||||
        });
 | 
			
		||||
        if (data.done) refreshWindow();
 | 
			
		||||
      }
 | 
			
		||||
    };
 | 
			
		||||
    asyncProcess();
 | 
			
		||||
  }, [process]);
 | 
			
		||||
 | 
			
		||||
  const handleRecaptcha = async (value) => {
 | 
			
		||||
    if (typeof value !== 'string') return;
 | 
			
		||||
    const [username, password] = getInputs();
 | 
			
		||||
    const d = await auth.completeRegisterStage(baseUrl, username, password, {
 | 
			
		||||
      type: 'm.login.recaptcha',
 | 
			
		||||
      response: value,
 | 
			
		||||
      session,
 | 
			
		||||
    });
 | 
			
		||||
    if (d.done) refreshWindow();
 | 
			
		||||
    else setProcess({ type: 'processing', message: 'Registration in progress....' });
 | 
			
		||||
  };
 | 
			
		||||
  const handleTerms = async () => {
 | 
			
		||||
    const [username, password] = getInputs();
 | 
			
		||||
    const d = await auth.completeRegisterStage(baseUrl, username, password, {
 | 
			
		||||
      type: 'm.login.terms',
 | 
			
		||||
      session,
 | 
			
		||||
    });
 | 
			
		||||
    if (d.done) refreshWindow();
 | 
			
		||||
    else setProcess({ type: 'processing', message: 'Registration in progress....' });
 | 
			
		||||
  };
 | 
			
		||||
  const handleEmailVerify = async () => {
 | 
			
		||||
    const [username, password] = getInputs();
 | 
			
		||||
    const d = await auth.completeRegisterStage(baseUrl, username, password, {
 | 
			
		||||
      type: 'm.login.email.identity',
 | 
			
		||||
      threepidCreds: { sid, client_secret: clientSecret },
 | 
			
		||||
      threepid_creds: { sid, client_secret: clientSecret },
 | 
			
		||||
      session,
 | 
			
		||||
    });
 | 
			
		||||
    if (d.done) refreshWindow();
 | 
			
		||||
    else setProcess({ type: 'processing', message: 'Registration in progress....' });
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <>
 | 
			
		||||
      {process.type === 'processing' && <LoadingScreen message={process.message} />}
 | 
			
		||||
      {process.type === 'm.login.recaptcha' && <Recaptcha message="Please check the box below to proceed." sitekey={process.sitekey} onChange={handleRecaptcha} />}
 | 
			
		||||
      {process.type === 'm.login.terms' && <Terms url={process.url} onSubmit={handleTerms} />}
 | 
			
		||||
      {process.type === 'm.login.email.identity' && <EmailVerify email={process.email} onContinue={handleEmailVerify} />}
 | 
			
		||||
      <div className="auth-form__heading">
 | 
			
		||||
        {!isDisabled && <Text variant="h2">Register</Text>}
 | 
			
		||||
        {isDisabled && <Text className="auth-form__error">{registerInfo.error}</Text>}
 | 
			
		||||
      </div>
 | 
			
		||||
      {!isDisabled && (
 | 
			
		||||
        <Formik
 | 
			
		||||
          initialValues={initialValues}
 | 
			
		||||
          onSubmit={submitter}
 | 
			
		||||
          validate={validator}
 | 
			
		||||
        >
 | 
			
		||||
          {({
 | 
			
		||||
            values, errors, handleChange, handleSubmit, isSubmitting,
 | 
			
		||||
          }) => (
 | 
			
		||||
            <>
 | 
			
		||||
              {process.type === undefined && isSubmitting && <LoadingScreen message="Registration in progress..." />}
 | 
			
		||||
              <form className="auth-form" ref={formRef} onSubmit={handleSubmit}>
 | 
			
		||||
                <Input values={values.username} name="username" onChange={handleChange} label="Username" type="username" required />
 | 
			
		||||
                {errors.username && <Text className="auth-form__error" variant="b3">{errors.username}</Text>}
 | 
			
		||||
                <Input values={values.password} name="password" onChange={handleChange} label="Password" type="password" required />
 | 
			
		||||
                {errors.password && <Text className="auth-form__error" variant="b3">{errors.password}</Text>}
 | 
			
		||||
                <Input values={values.confirmPassword} name="confirmPassword" onChange={handleChange} label="Confirm password" type="password" required />
 | 
			
		||||
                {errors.confirmPassword && <Text className="auth-form__error" variant="b3">{errors.confirmPassword}</Text>}
 | 
			
		||||
                {isEmail && <Input values={values.email} name="email" onChange={handleChange} label={`Email${isEmailRequired ? '' : ' (optional)'}`} type="email" required={isEmailRequired} />}
 | 
			
		||||
                {errors.email && <Text className="auth-form__error" variant="b3">{errors.email}</Text>}
 | 
			
		||||
                {errors.other && <Text className="auth-form__error" variant="b3">{errors.other}</Text>}
 | 
			
		||||
                <div className="auth-form__btns">
 | 
			
		||||
                  <Button variant="primary" type="submit" disabled={isSubmitting}>Register</Button>
 | 
			
		||||
                </div>
 | 
			
		||||
              </form>
 | 
			
		||||
            </>
 | 
			
		||||
          )}
 | 
			
		||||
        </Formik>
 | 
			
		||||
      )}
 | 
			
		||||
      {isDisabled && ssoProviders && (
 | 
			
		||||
        <SSOButtons
 | 
			
		||||
          type={ssoProviders.type.match(/^m.login.(sso|cas)$/)[1]}
 | 
			
		||||
          identityProviders={ssoProviders.identity_providers}
 | 
			
		||||
          baseUrl={baseUrl}
 | 
			
		||||
        />
 | 
			
		||||
      )}
 | 
			
		||||
    </>
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
Register.propTypes = {
 | 
			
		||||
  registerInfo: PropTypes.shape({}).isRequired,
 | 
			
		||||
  loginFlow: PropTypes.arrayOf(
 | 
			
		||||
    PropTypes.shape({}),
 | 
			
		||||
  ).isRequired,
 | 
			
		||||
  baseUrl: PropTypes.string.isRequired,
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
function AuthCardCopy() {
 | 
			
		||||
  const [hsConfig, setHsConfig] = useState(null);
 | 
			
		||||
  const [type, setType] = useState('login');
 | 
			
		||||
 | 
			
		||||
  const handleHsChange = (info) => setHsConfig(info);
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <>
 | 
			
		||||
      <Homeserver onChange={handleHsChange} />
 | 
			
		||||
      { hsConfig !== null && (
 | 
			
		||||
        type === 'login'
 | 
			
		||||
          ? <Login loginFlow={hsConfig.login.flows} baseUrl={hsConfig.baseUrl} />
 | 
			
		||||
          : (
 | 
			
		||||
            <Register
 | 
			
		||||
              registerInfo={hsConfig.register}
 | 
			
		||||
              loginFlow={hsConfig.login.flows}
 | 
			
		||||
              baseUrl={hsConfig.baseUrl}
 | 
			
		||||
            />
 | 
			
		||||
          )
 | 
			
		||||
      )}
 | 
			
		||||
      { hsConfig !== null && (
 | 
			
		||||
        <Text variant="b2" className="auth-card__switch flex--center">
 | 
			
		||||
          {`${(type === 'login' ? 'Don\'t have' : 'Already have')} an account?`}
 | 
			
		||||
          <button
 | 
			
		||||
            type="button"
 | 
			
		||||
            style={{ color: 'var(--tc-link)', cursor: 'pointer', margin: '0 var(--sp-ultra-tight)' }}
 | 
			
		||||
            onClick={() => setType((type === 'login') ? 'register' : 'login')}
 | 
			
		||||
          >
 | 
			
		||||
            { type === 'login' ? ' Register' : ' Login' }
 | 
			
		||||
          </button>
 | 
			
		||||
        </Text>
 | 
			
		||||
      )}
 | 
			
		||||
    </>
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function StaticWrapper({ children }) {
 | 
			
		||||
function Auth() {
 | 
			
		||||
  const [loginToken, setLoginToken] = useState(getUrlPrams('loginToken'));
 | 
			
		||||
 | 
			
		||||
  useEffect(async () => {
 | 
			
		||||
    if (!loginToken) return;
 | 
			
		||||
    if (localStorage.getItem(cons.secretKey.BASE_URL) === undefined) {
 | 
			
		||||
      setLoginToken(null);
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
    const baseUrl = localStorage.getItem(cons.secretKey.BASE_URL);
 | 
			
		||||
    try {
 | 
			
		||||
      await auth.loginWithToken(baseUrl, loginToken);
 | 
			
		||||
 | 
			
		||||
      const { href } = window.location;
 | 
			
		||||
      window.location.replace(href.slice(0, href.indexOf('?')));
 | 
			
		||||
    } catch {
 | 
			
		||||
      setLoginToken(null);
 | 
			
		||||
    }
 | 
			
		||||
  }, []);
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <ScrollView invisible>
 | 
			
		||||
      <div className="auth__wrapper flex--center">
 | 
			
		||||
        <div className="auth-card">
 | 
			
		||||
          <div className="auth-card__interactive flex-v">
 | 
			
		||||
            <div className="app-ident flex">
 | 
			
		||||
              <img className="app-ident__logo noselect" src={CinnySvg} alt="Cinny logo" />
 | 
			
		||||
              <div className="app-ident__text flex-v--center">
 | 
			
		||||
                <Text variant="h2">Cinny</Text>
 | 
			
		||||
                <Text variant="b2">Yet another matrix client</Text>
 | 
			
		||||
      <div className="auth__base">
 | 
			
		||||
        <div className="auth__wrapper">
 | 
			
		||||
          {loginToken && <LoadingScreen message="Redirecting..." />}
 | 
			
		||||
          {!loginToken && (
 | 
			
		||||
            <div className="auth-card flex-v">
 | 
			
		||||
              <Header>
 | 
			
		||||
                <Avatar size="extra-small" imageSrc={CinnySvg} />
 | 
			
		||||
                <TitleWrapper>
 | 
			
		||||
                  <Text variant="h2">Cinny</Text>
 | 
			
		||||
                </TitleWrapper>
 | 
			
		||||
              </Header>
 | 
			
		||||
              <div className="auth-card__content">
 | 
			
		||||
                <AuthCardCopy />
 | 
			
		||||
              </div>
 | 
			
		||||
            </div>
 | 
			
		||||
            { children }
 | 
			
		||||
          </div>
 | 
			
		||||
          )}
 | 
			
		||||
        </div>
 | 
			
		||||
 | 
			
		||||
        <div className="auth-footer">
 | 
			
		||||
          <Text variant="b2">
 | 
			
		||||
            <a href="https://cinny.in" target="_blank" rel="noreferrer">About</a>
 | 
			
		||||
          </Text>
 | 
			
		||||
          <Text variant="b2">
 | 
			
		||||
            <a href="https://github.com/ajbura/cinny/releases" target="_blank" rel="noreferrer">{`v${cons.version}`}</a>
 | 
			
		||||
          </Text>
 | 
			
		||||
          <Text variant="b2">
 | 
			
		||||
            <a href="https://twitter.com/cinnyapp" target="_blank" rel="noreferrer">Twitter</a>
 | 
			
		||||
          </Text>
 | 
			
		||||
          <Text variant="b2">
 | 
			
		||||
            <a href="https://matrix.org" target="_blank" rel="noreferrer">Powered by Matrix</a>
 | 
			
		||||
          </Text>
 | 
			
		||||
        </div>
 | 
			
		||||
      </div>
 | 
			
		||||
    </ScrollView>
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
StaticWrapper.propTypes = {
 | 
			
		||||
  children: PropTypes.node.isRequired,
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
function LoadingScreen({ message }) {
 | 
			
		||||
  return (
 | 
			
		||||
    <ProcessWrapper>
 | 
			
		||||
| 
						 | 
				
			
			@ -396,7 +595,7 @@ Recaptcha.propTypes = {
 | 
			
		|||
function Terms({ url, onSubmit }) {
 | 
			
		||||
  return (
 | 
			
		||||
    <ProcessWrapper>
 | 
			
		||||
      <form onSubmit={() => onSubmit(undefined, true)}>
 | 
			
		||||
      <form onSubmit={(e) => { e.preventDefault(); onSubmit(); }}>
 | 
			
		||||
        <div style={{ margin: 'var(--sp-normal)', maxWidth: '450px' }}>
 | 
			
		||||
          <Text variant="h2">Agree with terms</Text>
 | 
			
		||||
          <div style={{ marginBottom: 'var(--sp-normal)' }} />
 | 
			
		||||
| 
						 | 
				
			
			@ -419,6 +618,27 @@ Terms.propTypes = {
 | 
			
		|||
  onSubmit: PropTypes.func.isRequired,
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
function EmailVerify({ email, onContinue }) {
 | 
			
		||||
  return (
 | 
			
		||||
    <ProcessWrapper>
 | 
			
		||||
      <div style={{ margin: 'var(--sp-normal)', maxWidth: '450px' }}>
 | 
			
		||||
        <Text variant="h2">Verify email</Text>
 | 
			
		||||
        <div style={{ margin: 'var(--sp-normal) 0' }}>
 | 
			
		||||
          <Text variant="b1">
 | 
			
		||||
            {'Please check your email '}
 | 
			
		||||
            <b>{`(${email})`}</b>
 | 
			
		||||
            {' and validate before continuing further.'}
 | 
			
		||||
          </Text>
 | 
			
		||||
        </div>
 | 
			
		||||
        <Button variant="primary" onClick={onContinue}>Continue</Button>
 | 
			
		||||
      </div>
 | 
			
		||||
    </ProcessWrapper>
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
EmailVerify.propTypes = {
 | 
			
		||||
  email: PropTypes.string.isRequired,
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
function ProcessWrapper({ children }) {
 | 
			
		||||
  return (
 | 
			
		||||
    <div className="process-wrapper">
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,156 +1,144 @@
 | 
			
		|||
.auth__wrapper {
 | 
			
		||||
.auth__base {
 | 
			
		||||
  --pattern-size: 48px;
 | 
			
		||||
  min-height: 100vh;
 | 
			
		||||
  padding: var(--sp-loose);
 | 
			
		||||
  background-color: var(--bg-surface-low);
 | 
			
		||||
 | 
			
		||||
  background-image: url("https://images.unsplash.com/photo-1562619371-b67725b6fde2?ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&ixlib=rb-1.2.1&auto=format&fit=crop&w=1950&q=80");
 | 
			
		||||
  background-size: cover;
 | 
			
		||||
  background-repeat: no-repeat;
 | 
			
		||||
  background-position: center;
 | 
			
		||||
  background-image: radial-gradient(rgba(0, 0, 0, 6%) 2px, rgba(0, 0, 0, 0%) 2px);
 | 
			
		||||
  background-size: var(--pattern-size) var(--pattern-size);
 | 
			
		||||
 | 
			
		||||
  .auth-card {
 | 
			
		||||
    width: 462px;
 | 
			
		||||
    min-height: 644px;
 | 
			
		||||
    background-color: var(--bg-surface-low);
 | 
			
		||||
    border-radius: var(--bo-radius);
 | 
			
		||||
    box-shadow: var(--bs-popup);
 | 
			
		||||
    overflow: hidden;
 | 
			
		||||
    display: flex;
 | 
			
		||||
    flex-flow: row nowrap;
 | 
			
		||||
    
 | 
			
		||||
    &__interactive{
 | 
			
		||||
      flex: 1;
 | 
			
		||||
      min-width: 0;
 | 
			
		||||
    }
 | 
			
		||||
  display: flex;
 | 
			
		||||
  flex-direction: column;
 | 
			
		||||
}
 | 
			
		||||
.auth__wrapper {
 | 
			
		||||
  flex: 1;
 | 
			
		||||
  padding: var(--sp-loose);
 | 
			
		||||
  padding-bottom: 0;
 | 
			
		||||
  display: flex;
 | 
			
		||||
  justify-content: center;
 | 
			
		||||
  align-items: flex-start;
 | 
			
		||||
}
 | 
			
		||||
.auth-footer {
 | 
			
		||||
  padding: var(--sp-normal) 0;
 | 
			
		||||
  display: flex;
 | 
			
		||||
  justify-content: center;
 | 
			
		||||
  align-items: center;
 | 
			
		||||
 | 
			
		||||
    &__interactive {
 | 
			
		||||
      padding: calc(var(--sp-normal) + var(--sp-extra-loose));
 | 
			
		||||
      padding-bottom: var(--sp-extra-loose);
 | 
			
		||||
      background-color: var(--bg-surface);
 | 
			
		||||
    }
 | 
			
		||||
  & > *:nth-child(2n) {
 | 
			
		||||
    margin: 0 var(--sp-loose);
 | 
			
		||||
  }
 | 
			
		||||
  & a {
 | 
			
		||||
    color: var(--tc-surface-normal);
 | 
			
		||||
    &:hover { text-decoration: underline; }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
.auth-card {
 | 
			
		||||
  width: 462px;
 | 
			
		||||
  background-color: var(--bg-surface);
 | 
			
		||||
  border-radius: var(--bo-radius);
 | 
			
		||||
  box-shadow: var(--bs-popup);
 | 
			
		||||
  overflow: hidden;
 | 
			
		||||
 | 
			
		||||
  &__content {
 | 
			
		||||
    padding: var(--sp-extra-loose) calc(var(--sp-normal) + var(--sp-extra-loose));
 | 
			
		||||
  }
 | 
			
		||||
  &__switch {
 | 
			
		||||
    margin-top: var(--sp-loose) !important;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.app-ident {
 | 
			
		||||
  margin-bottom: var(--sp-extra-loose);
 | 
			
		||||
 | 
			
		||||
  &__logo {
 | 
			
		||||
    width: 60px;
 | 
			
		||||
    height: 60px;
 | 
			
		||||
.homeserver-form,
 | 
			
		||||
.auth-form__heading {
 | 
			
		||||
  & .context-menu .btn-surface .ic-raw {
 | 
			
		||||
    width: 0;
 | 
			
		||||
  }
 | 
			
		||||
  &__text {
 | 
			
		||||
    margin-left: calc(var(--sp-loose) + var(--sp-ultra-tight));
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
    .text-s1 {
 | 
			
		||||
      margin-top: var(--sp-tight);
 | 
			
		||||
      color: var(--tc-surface-normal);
 | 
			
		||||
.homeserver-form {
 | 
			
		||||
  display: flex;
 | 
			
		||||
  margin-bottom: var(--sp-extra-tight);
 | 
			
		||||
  & > .input-container {
 | 
			
		||||
    flex: 1;
 | 
			
		||||
    & .input {
 | 
			
		||||
      border-right: unset;
 | 
			
		||||
      border-radius: var(--bo-radius) 0 0 var(--bo-radius);
 | 
			
		||||
      background-color: var(--bg-surface);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  & .ic-btn {
 | 
			
		||||
    height: 46px;
 | 
			
		||||
    align-self: flex-end;
 | 
			
		||||
    border: 1px solid var(--bg-surface-border);
 | 
			
		||||
    border-radius: 0 var(--bo-radius) var(--bo-radius) 0;
 | 
			
		||||
  }
 | 
			
		||||
  [dir=rtl] & {
 | 
			
		||||
    & .input {
 | 
			
		||||
      border-radius: 0 var(--bo-radius) var(--bo-radius) 0;
 | 
			
		||||
      border-radius: 1px;
 | 
			
		||||
      border-left: unset;
 | 
			
		||||
    }
 | 
			
		||||
    .ic-btn {
 | 
			
		||||
      border-radius: var(--bo-radius) 0 0 var(--bo-radius);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
    [dir=rtl] & {
 | 
			
		||||
      margin-left: 0;
 | 
			
		||||
      margin-right:  calc(var(--sp-loose) + var(--sp-ultra-tight));
 | 
			
		||||
  &__status {
 | 
			
		||||
    margin-top: var(--sp-normal);
 | 
			
		||||
    & .donut-spinner {
 | 
			
		||||
      min-width: 28px;
 | 
			
		||||
    }
 | 
			
		||||
    & .text {
 | 
			
		||||
      margin: 0 var(--sp-tight);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  &__error {
 | 
			
		||||
    margin-bottom: var(--sp-normal) !important;
 | 
			
		||||
    color: var(--tc-danger-normal) !important;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.auth-form {
 | 
			
		||||
 | 
			
		||||
  & > .text {
 | 
			
		||||
    margin-bottom: var(--sp-loose);
 | 
			
		||||
    margin-top: var(--sp-loose);
 | 
			
		||||
  }
 | 
			
		||||
  & > .input-container {
 | 
			
		||||
    margin-top: var(--sp-tight);
 | 
			
		||||
    margin: var(--sp-tight) 0 var(--sp-ultra-tight);
 | 
			
		||||
  }
 | 
			
		||||
  
 | 
			
		||||
  &__heading {
 | 
			
		||||
    display: flex;
 | 
			
		||||
    justify-content: space-between;
 | 
			
		||||
    margin-top: calc(var(--sp-extra-loose) + var(--sp-tight));
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .submit-btn__wrapper {
 | 
			
		||||
    margin-top: var(--sp-extra-loose);
 | 
			
		||||
    margin-bottom: var(--sp-loose);
 | 
			
		||||
    align-items: flex-start;
 | 
			
		||||
 | 
			
		||||
    & > .error-message {
 | 
			
		||||
      display: none;
 | 
			
		||||
      flex: 1;
 | 
			
		||||
      color: var(--tc-danger-normal);
 | 
			
		||||
      margin-right: var(--sp-normal);
 | 
			
		||||
      word-break: break;
 | 
			
		||||
 | 
			
		||||
      [dir=rtl] & {
 | 
			
		||||
        margin: {
 | 
			
		||||
          right: 0;
 | 
			
		||||
          left: var(--sp-normal);
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  &__btns {
 | 
			
		||||
    padding-top: var(--sp-loose);
 | 
			
		||||
    margin-bottom: var(--sp-extra-loose);
 | 
			
		||||
    display: flex;
 | 
			
		||||
    justify-content: flex-end;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  &__wrapper {
 | 
			
		||||
    height: 100%;
 | 
			
		||||
  &__error {
 | 
			
		||||
    color: var(--tc-danger-normal) !important;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.username__wrapper {
 | 
			
		||||
.sso__divider {
 | 
			
		||||
  margin-bottom: var(--sp-tight);
 | 
			
		||||
  display: flex;
 | 
			
		||||
  align-items: flex-end;
 | 
			
		||||
  align-items: center;
 | 
			
		||||
 | 
			
		||||
  & > :first-child {
 | 
			
		||||
  &::before,
 | 
			
		||||
  &::after {
 | 
			
		||||
    flex: 1;
 | 
			
		||||
 | 
			
		||||
    .input {
 | 
			
		||||
      border-radius: var(--bo-radius) 0 0 var(--bo-radius);
 | 
			
		||||
 | 
			
		||||
      [dir=rtl] & {
 | 
			
		||||
        border-radius:  0 var(--bo-radius) var(--bo-radius) 0;
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  & > :last-child {
 | 
			
		||||
    width: 110px;
 | 
			
		||||
 | 
			
		||||
    .input {
 | 
			
		||||
      border-left-width: 0;
 | 
			
		||||
      background-color: var(--bg-surface);
 | 
			
		||||
      border-radius: 0 var(--bo-radius) var(--bo-radius) 0;
 | 
			
		||||
 | 
			
		||||
      [dir=rtl] & {
 | 
			
		||||
        border-left-width: 1px;
 | 
			
		||||
        border-right-width: 0;
 | 
			
		||||
        border-radius: var(--bo-radius) 0 0 var(--bo-radius);
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.password__wrapper {
 | 
			
		||||
  margin-top: var(--sp-tight);
 | 
			
		||||
  position: relative;
 | 
			
		||||
 | 
			
		||||
  & .ic-btn {
 | 
			
		||||
    position: absolute;
 | 
			
		||||
    right: 6px;
 | 
			
		||||
    bottom: 6px;
 | 
			
		||||
    border-radius: calc(var(--bo-radius) / 2);
 | 
			
		||||
    [dir=rtl] & {
 | 
			
		||||
      left: 6px;
 | 
			
		||||
      right: unset;
 | 
			
		||||
    }
 | 
			
		||||
    content: '';
 | 
			
		||||
    margin: var(--sp-tight);
 | 
			
		||||
    border-bottom: 1px solid var(--bg-surface-border);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@media (max-width: 462px) {
 | 
			
		||||
  .auth__wrapper {
 | 
			
		||||
    padding: 0;
 | 
			
		||||
    background-image: none;
 | 
			
		||||
    background-color: var(--bg-surface);
 | 
			
		||||
 | 
			
		||||
    .auth-card {
 | 
			
		||||
      border-radius: 0;
 | 
			
		||||
      box-shadow: none;
 | 
			
		||||
 | 
			
		||||
      &__interactive {
 | 
			
		||||
        padding: var(--sp-extra-loose);
 | 
			
		||||
      }
 | 
			
		||||
    padding: var(--sp-tight);
 | 
			
		||||
  }
 | 
			
		||||
  .auth-card {
 | 
			
		||||
    &__content {
 | 
			
		||||
      padding: var(--sp-loose) var(--sp-normal);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,189 +1,101 @@
 | 
			
		|||
import * as sdk from 'matrix-js-sdk';
 | 
			
		||||
import cons from '../state/cons';
 | 
			
		||||
import { getBaseUrl } from '../../util/matrixUtil';
 | 
			
		||||
 | 
			
		||||
// This method inspired by a similar one in matrix-react-sdk
 | 
			
		||||
async function createTemporaryClient(homeserver) {
 | 
			
		||||
  let baseUrl = null;
 | 
			
		||||
  try {
 | 
			
		||||
    baseUrl = await getBaseUrl(homeserver);
 | 
			
		||||
  } catch (e) {
 | 
			
		||||
    baseUrl = `https://${homeserver}`;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  if (typeof baseUrl === 'undefined') throw new Error('Homeserver not found');
 | 
			
		||||
function updateLocalStore(accessToken, deviceId, userId, baseUrl) {
 | 
			
		||||
  localStorage.setItem(cons.secretKey.ACCESS_TOKEN, accessToken);
 | 
			
		||||
  localStorage.setItem(cons.secretKey.DEVICE_ID, deviceId);
 | 
			
		||||
  localStorage.setItem(cons.secretKey.USER_ID, userId);
 | 
			
		||||
  localStorage.setItem(cons.secretKey.BASE_URL, baseUrl);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function createTemporaryClient(baseUrl) {
 | 
			
		||||
  return sdk.createClient({ baseUrl });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async function getLoginFlows(client) {
 | 
			
		||||
  const flows = await client.loginFlows();
 | 
			
		||||
  if (flows !== undefined) {
 | 
			
		||||
    return flows;
 | 
			
		||||
  }
 | 
			
		||||
  return null;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async function startSsoLogin(homeserver, type, idpId) {
 | 
			
		||||
  const client = await createTemporaryClient(homeserver);
 | 
			
		||||
async function startSsoLogin(baseUrl, type, idpId) {
 | 
			
		||||
  const client = createTemporaryClient(baseUrl);
 | 
			
		||||
  localStorage.setItem(cons.secretKey.BASE_URL, client.baseUrl);
 | 
			
		||||
  window.location.href = client.getSsoLoginUrl(window.location.href, type, idpId);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async function login(username, homeserver, password) {
 | 
			
		||||
  const client = await createTemporaryClient(homeserver);
 | 
			
		||||
async function login(baseUrl, username, email, password) {
 | 
			
		||||
  const identifier = {};
 | 
			
		||||
  if (username) {
 | 
			
		||||
    identifier.type = 'm.id.user';
 | 
			
		||||
    identifier.user = username;
 | 
			
		||||
  } else if (email) {
 | 
			
		||||
    identifier.type = 'm.id.thirdparty';
 | 
			
		||||
    identifier.medium = 'email';
 | 
			
		||||
    identifier.address = email;
 | 
			
		||||
  } else throw new Error('Bad Input');
 | 
			
		||||
 | 
			
		||||
  const response = await client.login('m.login.password', {
 | 
			
		||||
    identifier: {
 | 
			
		||||
      type: 'm.id.user',
 | 
			
		||||
      user: username,
 | 
			
		||||
    },
 | 
			
		||||
  const client = createTemporaryClient(baseUrl);
 | 
			
		||||
  const res = await client.login('m.login.password', {
 | 
			
		||||
    identifier,
 | 
			
		||||
    password,
 | 
			
		||||
    initial_device_display_name: cons.DEVICE_DISPLAY_NAME,
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  localStorage.setItem(cons.secretKey.ACCESS_TOKEN, response.access_token);
 | 
			
		||||
  localStorage.setItem(cons.secretKey.DEVICE_ID, response.device_id);
 | 
			
		||||
  localStorage.setItem(cons.secretKey.USER_ID, response.user_id);
 | 
			
		||||
  localStorage.setItem(cons.secretKey.BASE_URL, response?.well_known?.['m.homeserver']?.base_url || client.baseUrl);
 | 
			
		||||
  const myBaseUrl = res?.well_known?.['m.homeserver']?.base_url || client.baseUrl;
 | 
			
		||||
  updateLocalStore(res.access_token, res.device_id, res.user_id, myBaseUrl);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async function loginWithToken(baseUrl, token) {
 | 
			
		||||
  const client = sdk.createClient(baseUrl);
 | 
			
		||||
  const client = createTemporaryClient(baseUrl);
 | 
			
		||||
 | 
			
		||||
  const response = await client.login('m.login.token', {
 | 
			
		||||
  const res = await client.login('m.login.token', {
 | 
			
		||||
    token,
 | 
			
		||||
    initial_device_display_name: cons.DEVICE_DISPLAY_NAME,
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  localStorage.setItem(cons.secretKey.ACCESS_TOKEN, response.access_token);
 | 
			
		||||
  localStorage.setItem(cons.secretKey.DEVICE_ID, response.device_id);
 | 
			
		||||
  localStorage.setItem(cons.secretKey.USER_ID, response.user_id);
 | 
			
		||||
  localStorage.setItem(cons.secretKey.BASE_URL, response?.well_known?.['m.homeserver']?.base_url || client.baseUrl);
 | 
			
		||||
  const myBaseUrl = res?.well_known?.['m.homeserver']?.base_url || client.baseUrl;
 | 
			
		||||
  updateLocalStore(res.access_token, res.device_id, res.user_id, myBaseUrl);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async function getAdditionalInfo(baseUrl, content) {
 | 
			
		||||
  try {
 | 
			
		||||
    const res = await fetch(`${baseUrl}/_matrix/client/r0/register`, {
 | 
			
		||||
      method: 'POST',
 | 
			
		||||
      body: JSON.stringify(content),
 | 
			
		||||
      headers: {
 | 
			
		||||
        'Content-Type': 'application/json; charset=utf-8',
 | 
			
		||||
      },
 | 
			
		||||
      credentials: 'same-origin',
 | 
			
		||||
    });
 | 
			
		||||
    const data = await res.json();
 | 
			
		||||
    return data;
 | 
			
		||||
  } catch (e) {
 | 
			
		||||
    throw new Error(e);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async function verifyEmail(baseUrl, content) {
 | 
			
		||||
  try {
 | 
			
		||||
    const res = await fetch(`${baseUrl}/_matrix/client/r0/register/email/requestToken`, {
 | 
			
		||||
      method: 'POST',
 | 
			
		||||
      body: JSON.stringify(content),
 | 
			
		||||
      headers: {
 | 
			
		||||
        'Content-Type': 'application/json; charset=utf-8',
 | 
			
		||||
      },
 | 
			
		||||
      credentials: 'same-origin',
 | 
			
		||||
    });
 | 
			
		||||
    const data = await res.json();
 | 
			
		||||
    return data;
 | 
			
		||||
  } catch (e) {
 | 
			
		||||
    throw new Error(e);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
let session = null;
 | 
			
		||||
let clientSecret = null;
 | 
			
		||||
let sid = null;
 | 
			
		||||
async function register(username, homeserver, password, email, recaptchaValue, terms, verified) {
 | 
			
		||||
  const baseUrl = await getBaseUrl(homeserver);
 | 
			
		||||
 | 
			
		||||
  if (typeof baseUrl === 'undefined') throw new Error('Homeserver not found');
 | 
			
		||||
 | 
			
		||||
  const client = sdk.createClient({ baseUrl });
 | 
			
		||||
 | 
			
		||||
  const isAvailable = await client.isUsernameAvailable(username);
 | 
			
		||||
  if (!isAvailable) throw new Error('Username not available');
 | 
			
		||||
 | 
			
		||||
  if (typeof recaptchaValue === 'string') {
 | 
			
		||||
    await getAdditionalInfo(baseUrl, {
 | 
			
		||||
      auth: {
 | 
			
		||||
        type: 'm.login.recaptcha',
 | 
			
		||||
        session,
 | 
			
		||||
        response: recaptchaValue,
 | 
			
		||||
      },
 | 
			
		||||
    });
 | 
			
		||||
  } else if (terms === true) {
 | 
			
		||||
    await getAdditionalInfo(baseUrl, {
 | 
			
		||||
      auth: {
 | 
			
		||||
        type: 'm.login.terms',
 | 
			
		||||
        session,
 | 
			
		||||
      },
 | 
			
		||||
    });
 | 
			
		||||
  } else if (verified !== true) {
 | 
			
		||||
    session = null;
 | 
			
		||||
    clientSecret = client.generateClientSecret();
 | 
			
		||||
    const verifyData = await verifyEmail(baseUrl, {
 | 
			
		||||
      email,
 | 
			
		||||
      client_secret: clientSecret,
 | 
			
		||||
      send_attempt: 1,
 | 
			
		||||
    });
 | 
			
		||||
    if (typeof verifyData.error === 'string') {
 | 
			
		||||
      throw new Error(verifyData.error);
 | 
			
		||||
    }
 | 
			
		||||
    sid = verifyData.sid;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const additionalInfo = await getAdditionalInfo(baseUrl, {
 | 
			
		||||
    auth: { session: (session !== null) ? session : undefined },
 | 
			
		||||
// eslint-disable-next-line camelcase
 | 
			
		||||
async function verifyEmail(baseUrl, email, client_secret, send_attempt, next_link) {
 | 
			
		||||
  const res = await fetch(`${baseUrl}/_matrix/client/r0/register/email/requestToken`, {
 | 
			
		||||
    method: 'POST',
 | 
			
		||||
    body: JSON.stringify({
 | 
			
		||||
      email, client_secret, send_attempt, next_link,
 | 
			
		||||
    }),
 | 
			
		||||
    headers: {
 | 
			
		||||
      'Content-Type': 'application/json; charset=utf-8',
 | 
			
		||||
    },
 | 
			
		||||
    credentials: 'same-origin',
 | 
			
		||||
  });
 | 
			
		||||
  session = additionalInfo.session;
 | 
			
		||||
  if (typeof additionalInfo.completed === 'undefined' || additionalInfo.completed.length === 0) {
 | 
			
		||||
    return ({
 | 
			
		||||
      type: 'recaptcha',
 | 
			
		||||
      public_key: additionalInfo.params['m.login.recaptcha'].public_key,
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
  if (additionalInfo.completed.find((process) => process === 'm.login.recaptcha') === 'm.login.recaptcha'
 | 
			
		||||
    && !additionalInfo.completed.find((process) => process === 'm.login.terms')) {
 | 
			
		||||
    return ({
 | 
			
		||||
      type: 'terms',
 | 
			
		||||
      en: additionalInfo.params['m.login.terms'].policies.privacy_policy.en,
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
  if (verified || additionalInfo.completed.find((process) => process === 'm.login.terms') === 'm.login.terms') {
 | 
			
		||||
    const tpc = {
 | 
			
		||||
      client_secret: clientSecret,
 | 
			
		||||
      sid,
 | 
			
		||||
    };
 | 
			
		||||
    const verifyData = await getAdditionalInfo(baseUrl, {
 | 
			
		||||
      auth: {
 | 
			
		||||
        session,
 | 
			
		||||
        type: 'm.login.email.identity',
 | 
			
		||||
        threepidCreds: tpc,
 | 
			
		||||
        threepid_creds: tpc,
 | 
			
		||||
      },
 | 
			
		||||
      username,
 | 
			
		||||
      password,
 | 
			
		||||
    });
 | 
			
		||||
    if (verifyData.errcode === 'M_UNAUTHORIZED') {
 | 
			
		||||
      return { type: 'email' };
 | 
			
		||||
    }
 | 
			
		||||
  const data = await res.json();
 | 
			
		||||
  return data;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
    localStorage.setItem(cons.secretKey.ACCESS_TOKEN, verifyData.access_token);
 | 
			
		||||
    localStorage.setItem(cons.secretKey.DEVICE_ID, verifyData.device_id);
 | 
			
		||||
    localStorage.setItem(cons.secretKey.USER_ID, verifyData.user_id);
 | 
			
		||||
    localStorage.setItem(cons.secretKey.BASE_URL, baseUrl);
 | 
			
		||||
    return { type: 'done' };
 | 
			
		||||
async function completeRegisterStage(
 | 
			
		||||
  baseUrl, username, password, auth,
 | 
			
		||||
) {
 | 
			
		||||
  const tempClient = createTemporaryClient(baseUrl);
 | 
			
		||||
 | 
			
		||||
  try {
 | 
			
		||||
    const result = await tempClient.registerRequest({
 | 
			
		||||
      username, password, auth,
 | 
			
		||||
    });
 | 
			
		||||
    const data = { completed: result.completed || [] };
 | 
			
		||||
    if (result.access_token) {
 | 
			
		||||
      data.done = true;
 | 
			
		||||
      updateLocalStore(result.access_token, result.device_id, result.user_id, baseUrl);
 | 
			
		||||
    }
 | 
			
		||||
    return data;
 | 
			
		||||
  } catch (e) {
 | 
			
		||||
    const result = e.data;
 | 
			
		||||
    const data = { completed: result.completed || [] };
 | 
			
		||||
    if (result.access_token) {
 | 
			
		||||
      data.done = true;
 | 
			
		||||
      updateLocalStore(result.access_token, result.device_id, result.user_id, baseUrl);
 | 
			
		||||
    }
 | 
			
		||||
    return data;
 | 
			
		||||
  }
 | 
			
		||||
  return {};
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export {
 | 
			
		||||
  createTemporaryClient, getLoginFlows, login,
 | 
			
		||||
  loginWithToken, register, startSsoLogin,
 | 
			
		||||
  createTemporaryClient, login, verifyEmail,
 | 
			
		||||
  loginWithToken, startSsoLogin,
 | 
			
		||||
  completeRegisterStage,
 | 
			
		||||
};
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,4 +1,5 @@
 | 
			
		|||
const cons = {
 | 
			
		||||
  version: '1.4.0',
 | 
			
		||||
  secretKey: {
 | 
			
		||||
    ACCESS_TOKEN: 'cinny_access_token',
 | 
			
		||||
    DEVICE_ID: 'cinny_device_id',
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -2,15 +2,18 @@ import initMatrix from '../client/initMatrix';
 | 
			
		|||
 | 
			
		||||
const WELL_KNOWN_URI = '/.well-known/matrix/client';
 | 
			
		||||
 | 
			
		||||
async function getBaseUrl(homeserver) {
 | 
			
		||||
  const serverDiscoveryUrl = `https://${homeserver}${WELL_KNOWN_URI}`;
 | 
			
		||||
async function getBaseUrl(servername) {
 | 
			
		||||
  let protocol = 'https://';
 | 
			
		||||
  if (servername.match(/^https?:\/\//) !== null) protocol = '';
 | 
			
		||||
  const serverDiscoveryUrl = `${protocol}${servername}${WELL_KNOWN_URI}`;
 | 
			
		||||
  try {
 | 
			
		||||
    const result = await fetch(serverDiscoveryUrl, { method: 'GET' });
 | 
			
		||||
    const data = await result.json();
 | 
			
		||||
    const result = await (await fetch(serverDiscoveryUrl, { method: 'GET' })).json();
 | 
			
		||||
 | 
			
		||||
    return data?.['m.homeserver']?.base_url;
 | 
			
		||||
    const baseUrl = result?.['m.homeserver']?.base_url;
 | 
			
		||||
    if (baseUrl === undefined) throw new Error();
 | 
			
		||||
    return baseUrl;
 | 
			
		||||
  } catch (e) {
 | 
			
		||||
    throw new Error('Homeserver not found');
 | 
			
		||||
    throw new Error(`${protocol}${servername}`);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue