Basic authentication forms (#347)
This commit is contained in:
parent
a293e95979
commit
a3527e0b77
9
.env
9
.env
|
@ -1,2 +1,9 @@
|
|||
BROWSER=none
|
||||
NPM_GITHUB_TOKEN=
|
||||
REACT_APP_FIREBASE_APP_SENTRY_DSN=
|
||||
REACT_APP_FIREBASE_API_KEY=
|
||||
REACT_APP_FIREBASE_AUTH_DOMAIN=
|
||||
REACT_APP_FIREBASE_PROJECT_ID=
|
||||
REACT_APP_FIREBASE_STORAGE_BUCKET=
|
||||
REACT_APP_FIREBASE_MESSAGING_SENDER_ID=
|
||||
REACT_APP_FIREBASE_APP_ID=
|
||||
REACT_APP_FIREBASE_MEASUREMENT_ID=
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -35,6 +35,7 @@
|
|||
"@sentry/tracing": "^6.16.1",
|
||||
"@speedy-tuner/types": "^0.2.1",
|
||||
"antd": "^4.17.4",
|
||||
"firebase": "^9.6.1",
|
||||
"mlg-converter": "^0.5.1",
|
||||
"pako": "^2.0.4",
|
||||
"react": "^17.0.1",
|
||||
|
|
|
@ -64,6 +64,10 @@ html, body {
|
|||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
.ant-checkbox-wrapper {
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.table {
|
||||
margin: 20px;
|
||||
|
||||
|
|
46
src/App.tsx
46
src/App.tsx
|
@ -10,6 +10,8 @@ import {
|
|||
import { Layout } from 'antd';
|
||||
import { connect } from 'react-redux';
|
||||
import {
|
||||
ReactNode,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useMemo,
|
||||
} from 'react';
|
||||
|
@ -32,6 +34,8 @@ import Log from './components/Log';
|
|||
import Diagnose from './components/Diagnose';
|
||||
import useStorage from './hooks/useStorage';
|
||||
import useConfig from './hooks/useConfig';
|
||||
import Login from './components/Auth/Login';
|
||||
import SignUp from './components/Auth/SignUp';
|
||||
|
||||
const { Content } = Layout;
|
||||
|
||||
|
@ -76,6 +80,20 @@ const App = ({ ui, config }: { ui: UIState, config: ConfigType }) => {
|
|||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
const ContentFor = useCallback((props: { children: ReactNode, marginLeft?: number }) => {
|
||||
const { children, marginLeft } = props;
|
||||
|
||||
return (
|
||||
<Layout style={{ marginLeft }}>
|
||||
<Layout className="app-content">
|
||||
<Content>
|
||||
{children}
|
||||
</Content>
|
||||
</Layout>
|
||||
</Layout>
|
||||
);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Layout>
|
||||
|
@ -102,22 +120,24 @@ const App = ({ ui, config }: { ui: UIState, config: ConfigType }) => {
|
|||
</Layout>
|
||||
</Route>
|
||||
<Route path={Routes.LOG}>
|
||||
<Layout style={{ marginLeft: margin }}>
|
||||
<Layout className="app-content">
|
||||
<Content>
|
||||
<ContentFor marginLeft={margin}>
|
||||
<Log />
|
||||
</Content>
|
||||
</Layout>
|
||||
</Layout>
|
||||
</ContentFor>
|
||||
</Route>
|
||||
<Route>
|
||||
<Layout style={{ marginLeft: margin }}>
|
||||
<Layout className="app-content">
|
||||
<Content>
|
||||
<Route path={Routes.DIAGNOSE}>
|
||||
<ContentFor marginLeft={margin}>
|
||||
<Diagnose />
|
||||
</Content>
|
||||
</Layout>
|
||||
</Layout>
|
||||
</ContentFor>
|
||||
</Route>
|
||||
<Route path={Routes.LOGIN}>
|
||||
<ContentFor>
|
||||
<Login />
|
||||
</ContentFor>
|
||||
</Route>
|
||||
<Route path={Routes.SIGN_UP}>
|
||||
<ContentFor>
|
||||
<SignUp />
|
||||
</ContentFor>
|
||||
</Route>
|
||||
</Switch>
|
||||
</Layout>
|
||||
|
|
|
@ -0,0 +1,92 @@
|
|||
import { useState } from 'react';
|
||||
import {
|
||||
Form,
|
||||
Input,
|
||||
Button,
|
||||
Divider,
|
||||
notification,
|
||||
} from 'antd';
|
||||
import {
|
||||
MailOutlined,
|
||||
LockOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { useAuth } from '../../contexts/AuthContext';
|
||||
import { Routes } from '../../routes';
|
||||
import validateMessages from './validateMessages';
|
||||
|
||||
const { Item } = Form;
|
||||
|
||||
const Login = () => {
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const { login } = useAuth();
|
||||
|
||||
const onFinish = async ({ email, password }: { email: string, password: string }) => {
|
||||
setIsLoading(true);
|
||||
|
||||
try {
|
||||
await login(email, password);
|
||||
} catch (err) {
|
||||
console.warn(err);
|
||||
notification.error({
|
||||
message: 'Login failed',
|
||||
description: (err as Error).message,
|
||||
});
|
||||
}
|
||||
|
||||
setIsLoading(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<div style={{
|
||||
padding: 20,
|
||||
maxWidth: 350,
|
||||
margin: '0 auto',
|
||||
}}>
|
||||
<Divider>Login</Divider>
|
||||
<Form
|
||||
initialValues={{ remember: true }}
|
||||
onFinish={onFinish}
|
||||
validateMessages={validateMessages}
|
||||
autoComplete="off"
|
||||
>
|
||||
<Item
|
||||
name="email"
|
||||
rules={[{ required: true, type: 'email' }]}
|
||||
hasFeedback
|
||||
>
|
||||
<Input
|
||||
prefix={<MailOutlined />}
|
||||
placeholder="Email"
|
||||
/>
|
||||
</Item>
|
||||
<Item
|
||||
name="password"
|
||||
rules={[{ required: true }]}
|
||||
hasFeedback
|
||||
>
|
||||
<Input.Password
|
||||
placeholder="Password"
|
||||
prefix={<LockOutlined />}
|
||||
/>
|
||||
</Item>
|
||||
<Item>
|
||||
<Button
|
||||
type="primary"
|
||||
htmlType="submit"
|
||||
style={{ width: '100%' }}
|
||||
loading={isLoading}
|
||||
>
|
||||
Log in
|
||||
</Button>
|
||||
</Item>
|
||||
<Link to={Routes.SIGN_UP}>Sign Up now!</Link>
|
||||
<Link to="/" style={{ float: 'right' }}>
|
||||
Forgot password?
|
||||
</Link>
|
||||
</Form>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Login;
|
|
@ -0,0 +1,115 @@
|
|||
import { useState } from 'react';
|
||||
import {
|
||||
Form,
|
||||
Input,
|
||||
Button,
|
||||
Divider,
|
||||
notification,
|
||||
} from 'antd';
|
||||
import {
|
||||
MailOutlined,
|
||||
LockOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { useAuth } from '../../contexts/AuthContext';
|
||||
import { Routes } from '../../routes';
|
||||
import validateMessages from './validateMessages';
|
||||
|
||||
const { Item } = Form;
|
||||
|
||||
const strongPassword = /^(?=.*[a-z])(?=.*[A-Z])(?=.*[0-9])(?=.*[!@#$%^&*])(?=.{8,})/;
|
||||
|
||||
const SignUp = () => {
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const { signUp } = useAuth();
|
||||
|
||||
const onFinish = async ({ email, password }: { email: string, password: string }) => {
|
||||
setIsLoading(true);
|
||||
|
||||
try {
|
||||
await signUp(email, password);
|
||||
} catch (err) {
|
||||
console.warn(err);
|
||||
notification.error({
|
||||
message: 'Failed to create an account',
|
||||
description: (err as Error).message,
|
||||
});
|
||||
}
|
||||
|
||||
setIsLoading(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<div style={{
|
||||
padding: 20,
|
||||
maxWidth: 350,
|
||||
margin: '0 auto',
|
||||
}}>
|
||||
<Divider>Sign Up</Divider>
|
||||
<Form
|
||||
initialValues={{ remember: true }}
|
||||
onFinish={onFinish}
|
||||
validateMessages={validateMessages}
|
||||
autoComplete="off"
|
||||
>
|
||||
<Item
|
||||
name="email"
|
||||
rules={[{ required: true, type: 'email' }]}
|
||||
hasFeedback
|
||||
>
|
||||
<Input
|
||||
prefix={<MailOutlined />}
|
||||
placeholder="Email"
|
||||
/>
|
||||
</Item>
|
||||
<Item
|
||||
name="password"
|
||||
rules={[
|
||||
{ required: true },
|
||||
{ pattern: strongPassword, message: 'Password is too weak!' },
|
||||
]}
|
||||
hasFeedback
|
||||
>
|
||||
<Input.Password
|
||||
placeholder="Password"
|
||||
prefix={<LockOutlined />}
|
||||
/>
|
||||
</Item>
|
||||
<Item
|
||||
name="passwordConfirmation"
|
||||
rules={[
|
||||
{ required: true },
|
||||
({ getFieldValue }) => ({
|
||||
validator(_, value) {
|
||||
if (!value || getFieldValue('password') === value) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
return Promise.reject(new Error('Passwords don\'t match!'));
|
||||
},
|
||||
}),
|
||||
]}
|
||||
hasFeedback
|
||||
>
|
||||
<Input.Password
|
||||
placeholder="Password confirmation"
|
||||
prefix={<LockOutlined />}
|
||||
/>
|
||||
</Item>
|
||||
<Item>
|
||||
<Button
|
||||
type="primary"
|
||||
htmlType="submit"
|
||||
style={{ width: '100%' }}
|
||||
loading={isLoading}
|
||||
>
|
||||
Sign up
|
||||
</Button>
|
||||
</Item>
|
||||
Or <Link to={Routes.LOGIN}>login</Link> if you already have an account!
|
||||
</Form>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default SignUp;
|
|
@ -0,0 +1,8 @@
|
|||
const validateMessages = {
|
||||
required: 'This field is required!',
|
||||
types: {
|
||||
email: 'This is not a valid email!',
|
||||
},
|
||||
};
|
||||
|
||||
export default validateMessages;
|
|
@ -5,7 +5,10 @@ import {
|
|||
Row,
|
||||
Col,
|
||||
} from 'antd';
|
||||
import { InfoCircleOutlined } from '@ant-design/icons';
|
||||
import {
|
||||
InfoCircleOutlined,
|
||||
FieldTimeOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import { connect } from 'react-redux';
|
||||
import {
|
||||
AppState,
|
||||
|
@ -22,19 +25,22 @@ const mapStateToProps = (state: AppState) => ({
|
|||
|
||||
const firmware = (signature: string) => (
|
||||
<Space>
|
||||
<InfoCircleOutlined />{signature}
|
||||
<InfoCircleOutlined />
|
||||
{signature}
|
||||
</Space>
|
||||
);
|
||||
|
||||
const StatusBar = ({ status, config }: { status: StatusState, config: ConfigState }) => (
|
||||
<Footer className="app-status-bar">
|
||||
<Row>
|
||||
<Col span={8} />
|
||||
<Col span={8} style={{ textAlign: 'center' }}>
|
||||
<Col span={12}>
|
||||
{config.megaTune && firmware(config.megaTune.signature)}
|
||||
</Col>
|
||||
<Col span={8} style={{ textAlign: 'right' }}>
|
||||
<Col span={12} style={{ textAlign: 'right' }}>
|
||||
<Space>
|
||||
<FieldTimeOutlined />
|
||||
{status.text}
|
||||
</Space>
|
||||
</Col>
|
||||
</Row>
|
||||
</Footer>
|
||||
|
|
|
@ -3,6 +3,7 @@ import {
|
|||
useLocation,
|
||||
useHistory,
|
||||
} from 'react-router';
|
||||
import { Link } from 'react-router-dom';
|
||||
import {
|
||||
Layout,
|
||||
Space,
|
||||
|
@ -38,6 +39,8 @@ import {
|
|||
SearchOutlined,
|
||||
ToolOutlined,
|
||||
FundOutlined,
|
||||
UserAddOutlined,
|
||||
LogoutOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import {
|
||||
useEffect,
|
||||
|
@ -51,6 +54,7 @@ import {
|
|||
isToggleSidebar,
|
||||
} from '../utils/keyboard/shortcuts';
|
||||
import { Routes } from '../routes';
|
||||
import { useAuth } from '../contexts/AuthContext';
|
||||
|
||||
const { Header } = Layout;
|
||||
const { useBreakpoint } = Grid;
|
||||
|
@ -59,16 +63,32 @@ const { SubMenu } = Menu;
|
|||
const TopBar = () => {
|
||||
const { sm } = useBreakpoint();
|
||||
const { pathname } = useLocation();
|
||||
const { currentUser, logout } = useAuth();
|
||||
const history = useHistory();
|
||||
const matchedTabPath = useMemo(() => matchPath(pathname, { path: Routes.TAB }), [pathname]);
|
||||
|
||||
const userMenu = (
|
||||
<Menu>
|
||||
<Menu.Item key="login" disabled icon={<LoginOutlined />}>
|
||||
Login / Sign-up
|
||||
{currentUser ? (
|
||||
<Menu.Item key="logout" icon={<LogoutOutlined />} onClick={logout}>
|
||||
Logout
|
||||
</Menu.Item>
|
||||
) : (
|
||||
<>
|
||||
<Menu.Item key="login" icon={<LoginOutlined />}>
|
||||
<Link to={Routes.LOGIN}>Login</Link>
|
||||
</Menu.Item>
|
||||
<Menu.Item key="sign-up" icon={<UserAddOutlined />}>
|
||||
<Link to={Routes.SIGN_UP}>Sign Up</Link>
|
||||
</Menu.Item>
|
||||
</>
|
||||
)}
|
||||
<Menu.Item key="gh" icon={<GithubOutlined />}>
|
||||
<a href="https://github.com/speedy-tuner/speedy-tuner-cloud" target="__blank" rel="noopener noreferrer">
|
||||
<a
|
||||
href="https://github.com/speedy-tuner/speedy-tuner-cloud"
|
||||
target="__blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
GitHub
|
||||
</a>
|
||||
</Menu.Item>
|
||||
|
|
|
@ -0,0 +1,59 @@
|
|||
import { UserCredential } from 'firebase/auth';
|
||||
import {
|
||||
createContext,
|
||||
ReactNode,
|
||||
useContext,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useState,
|
||||
} from 'react';
|
||||
import {
|
||||
auth,
|
||||
createUserWithEmailAndPassword,
|
||||
signInWithEmailAndPassword,
|
||||
signOut,
|
||||
} from '../firebase';
|
||||
|
||||
const AuthContext = createContext<any>(null);
|
||||
|
||||
interface AuthValue {
|
||||
currentUser?: UserCredential,
|
||||
signUp: (email: string, password: string) => Promise<UserCredential>,
|
||||
login: (email: string, password: string) => Promise<UserCredential>,
|
||||
logout: () => Promise<void>,
|
||||
}
|
||||
|
||||
const useAuth = () => useContext<AuthValue>(AuthContext);
|
||||
|
||||
const AuthProvider = (props: { children: ReactNode }) => {
|
||||
const { children } = props;
|
||||
const [currentUser, setCurrentUser] = useState<any | null>(null);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
|
||||
const value = useMemo(() => ({
|
||||
currentUser,
|
||||
signUp: (email: string, password: string) => createUserWithEmailAndPassword(auth, email, password),
|
||||
login: (email: string, password: string) => signInWithEmailAndPassword(auth, email, password),
|
||||
logout: () => signOut(auth),
|
||||
}), [currentUser]);
|
||||
|
||||
useEffect(() => {
|
||||
const unsubscribe = auth.onAuthStateChanged((user) => {
|
||||
setCurrentUser(user);
|
||||
setIsLoading(false);
|
||||
});
|
||||
|
||||
return unsubscribe;
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<AuthContext.Provider value={value}>
|
||||
{!isLoading && children}
|
||||
</AuthContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
export {
|
||||
useAuth,
|
||||
AuthProvider,
|
||||
};
|
|
@ -0,0 +1,30 @@
|
|||
import { initializeApp } from 'firebase/app';
|
||||
import {
|
||||
getAuth,
|
||||
createUserWithEmailAndPassword,
|
||||
signInWithEmailAndPassword,
|
||||
signOut,
|
||||
} from 'firebase/auth';
|
||||
import { getAnalytics } from 'firebase/analytics';
|
||||
|
||||
const firebaseConfig = {
|
||||
apiKey: process.env.REACT_APP_FIREBASE_API_KEY,
|
||||
authDomain: process.env.REACT_APP_FIREBASE_AUTH_DOMAIN,
|
||||
projectId: process.env.REACT_APP_FIREBASE_PROJECT_ID,
|
||||
storageBucket: process.env.REACT_APP_FIREBASE_STORAGE_BUCKET,
|
||||
messagingSenderId: process.env.REACT_APP_FIREBASE_MESSAGING_SENDER_ID,
|
||||
appId: process.env.REACT_APP_FIREBASE_APP_ID,
|
||||
measurementId: process.env.REACT_APP_FIREBASE_MEASUREMENT_ID,
|
||||
};
|
||||
|
||||
const firebase = initializeApp(firebaseConfig);
|
||||
const analytics = getAnalytics(firebase);
|
||||
const auth = getAuth(firebase);
|
||||
|
||||
export {
|
||||
auth,
|
||||
analytics,
|
||||
createUserWithEmailAndPassword,
|
||||
signInWithEmailAndPassword,
|
||||
signOut,
|
||||
};
|
|
@ -10,6 +10,7 @@ import {
|
|||
isProduction,
|
||||
sentryDsn,
|
||||
} from './utils/env';
|
||||
import { AuthProvider } from './contexts/AuthContext';
|
||||
|
||||
if (isProduction) {
|
||||
Sentry.init({
|
||||
|
@ -22,9 +23,11 @@ if (isProduction) {
|
|||
|
||||
ReactDOM.render(
|
||||
<HashRouter>
|
||||
<AuthProvider>
|
||||
<Provider store={store}>
|
||||
<App />
|
||||
</Provider>
|
||||
</AuthProvider>
|
||||
</HashRouter>,
|
||||
document.getElementById('root'),
|
||||
);
|
||||
|
|
|
@ -6,4 +6,8 @@ export enum Routes {
|
|||
DIALOG = '/tune/:category/:dialog',
|
||||
LOG = '/log',
|
||||
DIAGNOSE = '/diagnose',
|
||||
LOGIN = '/login',
|
||||
LOGOUT = '/logout',
|
||||
SIGN_UP = '/sign-up',
|
||||
FORGOT_PASSWORD = '/forgot-password',
|
||||
}
|
||||
|
|
|
@ -91,6 +91,7 @@ export const loadLogs = (onProgress?: onProgressType, signal?: AbortSignal) =>
|
|||
export const loadCompositeLogs = (onProgress?: onProgressType, signal?: AbortSignal) =>
|
||||
fetchWithProgress(
|
||||
'https://d29mjpbgm6k6md.cloudfront.net/trigger-logs/composite_1_2.csv.gz',
|
||||
// 'https://d29mjpbgm6k6md.cloudfront.net/trigger-logs/composite_miata.csv.gz',
|
||||
// 'https://d29mjpbgm6k6md.cloudfront.net/trigger-logs/2.csv.gz',
|
||||
onProgress,
|
||||
signal,
|
||||
|
|
Loading…
Reference in New Issue