diff --git a/README.md b/README.md index a2ba622..35e339d 100644 --- a/README.md +++ b/README.md @@ -3,11 +3,16 @@ Ararat International School - платформа по обучению игры в шахматы. ### Стэк: -- Tailwind css - React.js - Express.js +- Tailwind css +- TypeScript - Mobx (временно) +#### V 0.01 +- Добавлена главная страница +- Регистрация и авторизация ведет на главую страницу +- Форма регистрации/авторизации из управляемых input теперь работает при помощи react-hook-form #### V 0.0 - Инициализация diff --git a/client/index.html b/client/index.html index b11052f..845afa2 100644 --- a/client/index.html +++ b/client/index.html @@ -4,6 +4,9 @@ + + + AraratChess diff --git a/client/package-lock.json b/client/package-lock.json index 1ccc4be..45a6e7f 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -9,11 +9,14 @@ "version": "0.0.0", "dependencies": { "@types/axios": "^0.14.0", + "@types/react-router-dom": "^5.3.3", "axios": "^1.4.0", "mobx": "^6.9.1", "mobx-react-lite": "^4.0.3", "react": "^18.2.0", - "react-dom": "^18.2.0" + "react-dom": "^18.2.0", + "react-hook-form": "^7.45.2", + "react-router-dom": "^6.14.2" }, "devDependencies": { "@types/react": "^18.2.14", @@ -957,6 +960,14 @@ "node": ">= 8" } }, + "node_modules/@remix-run/router": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.7.2.tgz", + "integrity": "sha512-7Lcn7IqGMV+vizMPoEl5F0XDshcdDYtMI6uJLQdQz5CfZAwy3vvGKYSUk789qndt5dEC4HfSjviSYlSoHGL2+A==", + "engines": { + "node": ">=14" + } + }, "node_modules/@types/axios": { "version": "0.14.0", "resolved": "https://registry.npmjs.org/@types/axios/-/axios-0.14.0.tgz", @@ -966,6 +977,11 @@ "axios": "*" } }, + "node_modules/@types/history": { + "version": "4.7.11", + "resolved": "https://registry.npmjs.org/@types/history/-/history-4.7.11.tgz", + "integrity": "sha512-qjDJRrmvBMiTx+jyLxvLfJU7UznFuokDv4f3WRuriHKERccVpFU+8XMQUAbDzoiJCsmexxRExQeMwwCdamSKDA==" + }, "node_modules/@types/json-schema": { "version": "7.0.12", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.12.tgz", @@ -975,14 +991,12 @@ "node_modules/@types/prop-types": { "version": "15.7.5", "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.5.tgz", - "integrity": "sha512-JCB8C6SnDoQf0cNycqd/35A7MjcnK+ZTqE7judS6o7utxUCg6imJg3QK2qzHKszlTjcj2cn+NwMB2i96ubpj7w==", - "dev": true + "integrity": "sha512-JCB8C6SnDoQf0cNycqd/35A7MjcnK+ZTqE7judS6o7utxUCg6imJg3QK2qzHKszlTjcj2cn+NwMB2i96ubpj7w==" }, "node_modules/@types/react": { "version": "18.2.15", "resolved": "https://registry.npmjs.org/@types/react/-/react-18.2.15.tgz", "integrity": "sha512-oEjE7TQt1fFTFSbf8kkNuc798ahTUzn3Le67/PWjE8MAfYAD/qB7O8hSTcromLFqHCt9bcdOg5GXMokzTjJ5SA==", - "dev": true, "dependencies": { "@types/prop-types": "*", "@types/scheduler": "*", @@ -998,11 +1012,29 @@ "@types/react": "*" } }, + "node_modules/@types/react-router": { + "version": "5.1.20", + "resolved": "https://registry.npmjs.org/@types/react-router/-/react-router-5.1.20.tgz", + "integrity": "sha512-jGjmu/ZqS7FjSH6owMcD5qpq19+1RS9DeVRqfl1FeBMxTDQAGwlMWOcs52NDoXaNKyG3d1cYQFMs9rCrb88o9Q==", + "dependencies": { + "@types/history": "^4.7.11", + "@types/react": "*" + } + }, + "node_modules/@types/react-router-dom": { + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/@types/react-router-dom/-/react-router-dom-5.3.3.tgz", + "integrity": "sha512-kpqnYK4wcdm5UaWI3fLcELopqLrHgLqNsdpHauzlQktfkHL3npOSwtj1Uz9oKBAzs7lFtVkV8j83voAz2D8fhw==", + "dependencies": { + "@types/history": "^4.7.11", + "@types/react": "*", + "@types/react-router": "*" + } + }, "node_modules/@types/scheduler": { "version": "0.16.3", "resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.3.tgz", - "integrity": "sha512-5cJ8CB4yAx7BH1oMvdU0Jh9lrEXyPkar6F9G/ERswkCuvP4KQZfZkSjcMbAICCpQTN4OuZn8tz0HiKv9TGZgrQ==", - "dev": true + "integrity": "sha512-5cJ8CB4yAx7BH1oMvdU0Jh9lrEXyPkar6F9G/ERswkCuvP4KQZfZkSjcMbAICCpQTN4OuZn8tz0HiKv9TGZgrQ==" }, "node_modules/@types/semver": { "version": "7.5.0", @@ -1598,8 +1630,7 @@ "node_modules/csstype": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.2.tgz", - "integrity": "sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ==", - "dev": true + "integrity": "sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ==" }, "node_modules/debug": { "version": "4.3.4", @@ -3078,6 +3109,21 @@ "react": "^18.2.0" } }, + "node_modules/react-hook-form": { + "version": "7.45.2", + "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.45.2.tgz", + "integrity": "sha512-9s45OdTaKN+4NSTbXVqeDITd/nwIg++nxJGL8+OD5uf1DxvhsXQ641kaYHk5K28cpIOTYm71O/fYk7rFaygb3A==", + "engines": { + "node": ">=12.22.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/react-hook-form" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17 || ^18" + } + }, "node_modules/react-refresh": { "version": "0.14.0", "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.14.0.tgz", @@ -3087,6 +3133,36 @@ "node": ">=0.10.0" } }, + "node_modules/react-router": { + "version": "6.14.2", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.14.2.tgz", + "integrity": "sha512-09Zss2dE2z+T1D03IheqAFtK4UzQyX8nFPWx6jkwdYzGLXd5ie06A6ezS2fO6zJfEb/SpG6UocN2O1hfD+2urQ==", + "dependencies": { + "@remix-run/router": "1.7.2" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "react": ">=16.8" + } + }, + "node_modules/react-router-dom": { + "version": "6.14.2", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.14.2.tgz", + "integrity": "sha512-5pWX0jdKR48XFZBuJqHosX3AAHjRAzygouMTyimnBPOLdY3WjzUSKhus2FVMihUFWzeLebDgr4r8UeQFAct7Bg==", + "dependencies": { + "@remix-run/router": "1.7.2", + "react-router": "6.14.2" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "react": ">=16.8", + "react-dom": ">=16.8" + } + }, "node_modules/read-cache": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", diff --git a/client/package.json b/client/package.json index 9e601e9..1b99a77 100644 --- a/client/package.json +++ b/client/package.json @@ -11,11 +11,14 @@ }, "dependencies": { "@types/axios": "^0.14.0", + "@types/react-router-dom": "^5.3.3", "axios": "^1.4.0", "mobx": "^6.9.1", "mobx-react-lite": "^4.0.3", "react": "^18.2.0", - "react-dom": "^18.2.0" + "react-dom": "^18.2.0", + "react-hook-form": "^7.45.2", + "react-router-dom": "^6.14.2" }, "devDependencies": { "@types/react": "^18.2.14", diff --git a/client/public/avatar.svg b/client/public/avatar.svg new file mode 100644 index 0000000..55b2b7c --- /dev/null +++ b/client/public/avatar.svg @@ -0,0 +1,4 @@ + + + + diff --git a/client/src/App.tsx b/client/src/App.tsx index a46e10d..980a84f 100644 --- a/client/src/App.tsx +++ b/client/src/App.tsx @@ -1,9 +1,38 @@ -import RegisterForm from './components/RegisterForm' +import { useEffect, useContext } from "react" +import RequireAuth from "./components/Auth/RequireAuth" +import OnlyUnauthorized from "./components/Auth/OnlyUnauthorized" +import IndexPage from "./pages/IndexPage" +import LoginForm from "./components/LoginForm" +import RegisterForm from "./components/RegisterForm" +import { BrowserRouter, Routes, Route } from 'react-router-dom' +import { Context } from "./main" +import { observer } from "mobx-react-lite" function App() { - return ( - - ) + const {store} = useContext(Context); + + useEffect(()=>{ + const CheckLogin = async () => { + if(localStorage.getItem('token')) { + await store.checkAuth(); + } else { + store.storeLoad(); + } + } + CheckLogin().catch(console.error); + }, []) + + if(!store.isLoading) { + return ( + + + }/> + }/> + }/> + + + ) + } } -export default App +export default observer(App) diff --git a/client/src/components/Auth/OnlyUnauthorized.tsx b/client/src/components/Auth/OnlyUnauthorized.tsx new file mode 100644 index 0000000..61502f9 --- /dev/null +++ b/client/src/components/Auth/OnlyUnauthorized.tsx @@ -0,0 +1,18 @@ +import { FC, PropsWithChildren, useContext } from 'react' +import { useLocation, Navigate } from 'react-router-dom'; +import { Context } from "../../main" +import { observer } from "mobx-react-lite" + +const OnlyUnauthorized: FC = observer(({children}) => { + + const {store} = useContext(Context); + const location = useLocation(); + + if (store.isAuth) { + return ; + } + + return children; +}) + +export default OnlyUnauthorized; \ No newline at end of file diff --git a/client/src/components/Auth/RequireAuth.tsx b/client/src/components/Auth/RequireAuth.tsx new file mode 100644 index 0000000..8407f67 --- /dev/null +++ b/client/src/components/Auth/RequireAuth.tsx @@ -0,0 +1,18 @@ +import { FC, PropsWithChildren, useContext } from 'react' +import { useLocation, Navigate } from 'react-router-dom'; +import { Context } from "../../main" +import { observer } from "mobx-react-lite" + +const RequireAuth: FC = observer(({children}) => { + + const {store} = useContext(Context); + const location = useLocation(); + + if (!store.isAuth) { + return ; + } + + return children; +}) + +export default RequireAuth; \ No newline at end of file diff --git a/client/src/components/Index/Sidebar/Menu.tsx b/client/src/components/Index/Sidebar/Menu.tsx new file mode 100644 index 0000000..26a57a0 --- /dev/null +++ b/client/src/components/Index/Sidebar/Menu.tsx @@ -0,0 +1,28 @@ +import { FC, useContext } from 'react' +import MenuItem from './MenuItem'; +import { Link } from 'react-router-dom'; +import { Context } from '../../../main'; + +const Menu: FC = () => { + const {store} = useContext(Context); + return ( + + ) +} + +export default Menu; \ No newline at end of file diff --git a/client/src/components/Index/Sidebar/MenuItem.tsx b/client/src/components/Index/Sidebar/MenuItem.tsx new file mode 100644 index 0000000..35ea4e3 --- /dev/null +++ b/client/src/components/Index/Sidebar/MenuItem.tsx @@ -0,0 +1,18 @@ +import { FC, PropsWithChildren } from 'react' +import { Link } from 'react-router-dom'; +import { useLocation } from 'react-router-dom'; + +interface MenuItemProps { + to: string, +} + +const MenuItem: FC> = ({to, children}) => { + + const location = useLocation(); + + return ( +
  • {children}
  • + ) +} + +export default MenuItem; \ No newline at end of file diff --git a/client/src/components/Index/Sidebar/Sidebar.tsx b/client/src/components/Index/Sidebar/Sidebar.tsx new file mode 100644 index 0000000..b2b686f --- /dev/null +++ b/client/src/components/Index/Sidebar/Sidebar.tsx @@ -0,0 +1,16 @@ +import { FC } from 'react' +import UserInfo from '../UserInfo'; +import Menu from './Menu'; +import Logo from '../../../assets/logo.png' + +const Sidebar: FC = () => { + return ( + + ) +} + +export default Sidebar; \ No newline at end of file diff --git a/client/src/components/Index/UserInfo.tsx b/client/src/components/Index/UserInfo.tsx new file mode 100644 index 0000000..02ff946 --- /dev/null +++ b/client/src/components/Index/UserInfo.tsx @@ -0,0 +1,21 @@ +import { useContext } from "react" +import { FC } from 'react' +import { Context } from "../../main" +import { observer } from "mobx-react-lite" + + +const UserInfo: FC = observer(() => { + const {store} = useContext(Context); + + return ( +
    +
    avatar
    +
    +

    {store.user.name + ' ' + store.user.sname}

    +

    {store.user.email}

    +
    +
    + ) +}) + +export default UserInfo; \ No newline at end of file diff --git a/client/src/components/LoginForm.tsx b/client/src/components/LoginForm.tsx index 8ccc844..8231c5f 100644 --- a/client/src/components/LoginForm.tsx +++ b/client/src/components/LoginForm.tsx @@ -1,30 +1,45 @@ /* eslint-disable @typescript-eslint/no-misused-promises */ -import {FC, useState, useContext} from 'react' +import {FC, useContext} from 'react' +import { useForm, SubmitHandler } from "react-hook-form"; import { Context } from '../main'; import Logo from '../assets/logo.png' +import { Link, useNavigate } from 'react-router-dom'; + +type Form = { + email: string, + password: string, +}; const LoginForm: FC = () => { - const [email, setEmail] = useState(''); - const [password, setPassword] = useState(''); + const navigate = useNavigate(); const {store} = useContext(Context); + + const { register, handleSubmit } = useForm
    (); + const onSubmit: SubmitHandler = async (data) => { + await store.login(data.email, data.password); + if(store.isAuth) { + navigate('/'); + } + } return (
    -
    + logo

    Login to your account

    Let's get you all set up so you can verify your personal account and begin setting up your profile.

    - setEmail(e.target.value)} value={email} type='email' placeholder='Email'/> +
    - setPassword(e.target.value)} value={password} type='password' placeholder='Password'/> +
    - -
    + +

    Dont have account? Sign up here.

    +
    ) } diff --git a/client/src/components/RegisterForm.tsx b/client/src/components/RegisterForm.tsx index 44b1331..fca3a36 100644 --- a/client/src/components/RegisterForm.tsx +++ b/client/src/components/RegisterForm.tsx @@ -1,40 +1,68 @@ /* eslint-disable @typescript-eslint/no-misused-promises */ -import {FC, useState, useContext} from 'react' +import {FC, useContext} from 'react' +import { useForm, SubmitHandler } from "react-hook-form"; +import { useNavigate, Link } from 'react-router-dom'; import { Context } from '../main'; import Logo from '../assets/logo.png' const RegisterForm: FC = () => { - const [email, setEmail] = useState(''); - const [name, setName] = useState(''); - const [sname, setSname] = useState(''); - const [password, setPassword] = useState(''); + type Form = { + email: string, + name: string, + sname: string, + password: string, + confirm_password: string + }; + + const navigate = useNavigate(); const {store} = useContext(Context); + const { register, handleSubmit, watch } = useForm
    (); + const onSubmit: SubmitHandler = async (data) => { + await store.registration(data.email, data.name, data.sname, data.password); + if(store.isAuth) { + navigate('/'); + } + } + return (
    -
    + logo

    Register To Get Started

    Let's get you all set up so you can verify your personal account and begin setting up your profile.

    - setEmail(e.target.value)} value={email} type='email' placeholder='Email'/> +
    - setName(e.target.value)} value={name} type='text' placeholder='First Name'/> +
    - setSname(e.target.value)} value={sname} type='text' placeholder='Last Name'/> +
    - setPassword(e.target.value)} value={password} type='password' placeholder='Password'/> + +
    +
    + + { + if (watch('password') != val) { + return "Your passwords do no match"; + } + }, + })} type='password' placeholder='Confirm Password'/>
    - -
    + +

    Already have an account ? Log In here.

    +
    ) } diff --git a/client/src/http/index.ts b/client/src/http/index.ts index 393d32d..8080ad1 100644 --- a/client/src/http/index.ts +++ b/client/src/http/index.ts @@ -1,6 +1,6 @@ import axios from "axios"; -export const API_URL = `https://chess.beknazaryanstudio.ru:8080/api` +export const API_URL = `http://localhost:8089/api` const $api = axios.create({ withCredentials: true, diff --git a/client/src/index.css b/client/src/index.css index bd6213e..171c37e 100644 --- a/client/src/index.css +++ b/client/src/index.css @@ -1,3 +1,7 @@ @tailwind base; @tailwind components; -@tailwind utilities; \ No newline at end of file +@tailwind utilities; + +html { + font-family: 'Manrope', sans-serif; +} \ No newline at end of file diff --git a/client/src/main.tsx b/client/src/main.tsx index 0ca102b..ebd1618 100644 --- a/client/src/main.tsx +++ b/client/src/main.tsx @@ -4,6 +4,7 @@ import App from './App.tsx' import './index.css' import Store from './store/store.ts' + interface State { store: Store } diff --git a/client/src/models/User.ts b/client/src/models/User.ts index 5ca08de..700148e 100644 --- a/client/src/models/User.ts +++ b/client/src/models/User.ts @@ -1,5 +1,8 @@ export interface User { _id: string; email: string; - role: string; + name: string; + sname: string; + avatar?: string; + role?: string; } \ No newline at end of file diff --git a/client/src/pages/IndexPage.tsx b/client/src/pages/IndexPage.tsx new file mode 100644 index 0000000..0c92eba --- /dev/null +++ b/client/src/pages/IndexPage.tsx @@ -0,0 +1,13 @@ +import { FC } from 'react' +import Sidebar from '../components/Index/Sidebar/Sidebar'; + +const IndexPage : FC = () => { + + return ( +
    + +
    + ) +} + +export default IndexPage; \ No newline at end of file diff --git a/client/src/store/store.ts b/client/src/store/store.ts index 9f8605e..a282a1d 100644 --- a/client/src/store/store.ts +++ b/client/src/store/store.ts @@ -1,10 +1,14 @@ import { User } from "../models/User" import { makeAutoObservable } from "mobx"; import AuthService from "../services/AuthService"; +import axios from "axios"; +import { AuthResponse } from "../models/response/AuthResponse"; +import { API_URL } from "../http"; export default class Store { user = {} as User; isAuth = false; + isLoading = true; constructor() { makeAutoObservable(this); @@ -18,10 +22,13 @@ export default class Store { this.user = user; } + setLoading(bool: boolean) { + this.isLoading = bool; + } + async registration (email: string, name: string, sname: string, password: string) { try { const response = await AuthService.registration(email, name, sname, password); - console.log(response); localStorage.setItem('token', response.data.accessToken); this.setAuth(true); this.setUser(response.data.user) @@ -33,7 +40,6 @@ export default class Store { async login (email: string, password: string) { try { const response = await AuthService.login(email, password); - console.log(response); localStorage.setItem('token', response.data.accessToken); this.setAuth(true); this.setUser(response.data.user) @@ -52,4 +58,22 @@ export default class Store { console.log(e) } } + + async checkAuth() { + this.setLoading(true); + try { + const response = await axios.get(`${API_URL}/auth/refresh`, {withCredentials: true}); + localStorage.setItem('token', response.data.accessToken); + this.setAuth(true); + this.setUser(response.data.user) + } catch (e) { + console.log(e); + } finally { + this.setLoading(false); + } + } + + storeLoad() { + this.setLoading(false); + } } \ No newline at end of file diff --git a/client/vite.config.ts b/client/vite.config.ts index 3683ffc..b751528 100644 --- a/client/vite.config.ts +++ b/client/vite.config.ts @@ -7,4 +7,7 @@ export default defineConfig({ server: { port: 3000, }, + preview: { + port: 3000, + } }) diff --git a/server/config/default.json b/server/config/default.json index 229ffe3..e511ec5 100644 --- a/server/config/default.json +++ b/server/config/default.json @@ -1,6 +1,6 @@ { "serverPort": 8089, - "dbUrl": "db", + "dbUrl": "dbUrl", "JWTAccessSecret": "jwt-ararat-access-sercet-dygqwuygoduwqygdqwugyid", "JWTRefreshSecret": "jwt-ararat-refresh-sercet-dqwyugfuftyiqwdutyfivd" } \ No newline at end of file diff --git a/server/dtos/UserDto.js b/server/dtos/UserDto.js index 2e3e779..4653b7c 100644 --- a/server/dtos/UserDto.js +++ b/server/dtos/UserDto.js @@ -1,11 +1,17 @@ export default class UserDto { _id; email; - role + name; + sname; + role; + avatar; constructor(model) { this._id = model._id; this.email = model.email; + this.name = model.name; + this.sname = model.sname; this.role = model.role; + this.avatar = model.avatar; } } diff --git a/server/models/User.js b/server/models/User.js index 97f18c5..50181ea 100644 --- a/server/models/User.js +++ b/server/models/User.js @@ -6,7 +6,7 @@ const User = new Schema({ sname: {type: String, required: true}, verify: {type: Boolean, default: false}, password: {type: String, required: true}, - avatar: {type: String}, + avatar: {type: String, default: '/avatar.svg'}, role: {type: String, default: "USER"}, });