diff options
| author | physcik <mynameisgennadiy@vk.com> | 2026-04-28 23:33:59 +0500 |
|---|---|---|
| committer | physcik <mynameisgennadiy@vk.com> | 2026-04-28 23:33:59 +0500 |
| commit | 533c04d9558bd0a575671c157ca42236f59a2c5a (patch) | |
| tree | 895ec8672f9633fd6cfef85f6a12921fc5339ab3 | |
| parent | ec5ef35ed47b90b9f09199d28f7885f8287815a8 (diff) | |
frontend auth
| -rw-r--r-- | backend/src/API/AuthenticationAPI.zig | 8 | ||||
| -rw-r--r-- | front/package-lock.json | 162 | ||||
| -rw-r--r-- | front/package.json | 2 | ||||
| -rwxr-xr-x | front/public/EBM.webp | bin | 0 -> 9976 bytes | |||
| -rw-r--r-- | front/src/App.css | 1 | ||||
| -rw-r--r-- | front/src/App.tsx | 42 | ||||
| -rw-r--r-- | front/src/Authentication/ContextProvider.ts | 38 | ||||
| -rw-r--r-- | front/src/Authentication/LoginPage.tsx | 60 | ||||
| -rw-r--r-- | front/src/Authentication/Models.ts | 4 | ||||
| -rw-r--r-- | front/src/Emelents/Elements.css | 7 | ||||
| -rw-r--r-- | front/src/Emelents/Sidebar.tsx | 1 | ||||
| -rw-r--r-- | front/src/Emelents/Topbar.tsx | 12 | ||||
| -rw-r--r-- | front/src/Locales/ru_RU.ts | 7 | ||||
| -rw-r--r-- | front/src/index.tsx | 10 |
14 files changed, 336 insertions, 18 deletions
diff --git a/backend/src/API/AuthenticationAPI.zig b/backend/src/API/AuthenticationAPI.zig index 80988aa..0c2a1e9 100644 --- a/backend/src/API/AuthenticationAPI.zig +++ b/backend/src/API/AuthenticationAPI.zig @@ -64,5 +64,11 @@ fn login(_: *Handler.RequestData, req: *httpz.Request, res: *httpz.Response) !vo }; const token = try Tokens.GenerateNewSession(res.arena, user); - try res.json(.{ .Token = token } , .{}); + try res.json(.{ + .Token = token, + .User = .{ + .Username = user.Username, + .Role = user.Role.ToString(), + }, + } , .{}); } diff --git a/front/package-lock.json b/front/package-lock.json index 6943f13..2e8fba5 100644 --- a/front/package-lock.json +++ b/front/package-lock.json @@ -18,9 +18,11 @@ "@types/react-dom": "^19.2.3", "axios": "^1.15.2", "react": "^19.2.5", + "react-cookie": "^8.1.0", "react-dom": "^19.2.5", "react-router": "^7.14.0", "react-scripts": "5.0.1", + "redis": "^5.12.1", "typescript": "^4.9.5", "web-vitals": "^2.1.4" } @@ -2992,6 +2994,78 @@ } } }, + "node_modules/@redis/bloom": { + "version": "5.12.1", + "resolved": "https://registry.npmjs.org/@redis/bloom/-/bloom-5.12.1.tgz", + "integrity": "sha512-PUUfv+ms7jgPSBVoo/DN4AkPHj4D5TZSd6SbJX7egzBplkYUcKmHRE8RKia7UtZ8bSQbLguLvxVO+asKtQfZWA==", + "license": "MIT", + "engines": { + "node": ">= 18.19.0" + }, + "peerDependencies": { + "@redis/client": "^5.12.1" + } + }, + "node_modules/@redis/client": { + "version": "5.12.1", + "resolved": "https://registry.npmjs.org/@redis/client/-/client-5.12.1.tgz", + "integrity": "sha512-7aPGWeqA3uFm43o19umzdl16CEjK/JQGtSXVPevplTaOU3VJA/rseBC1QvYUz9lLDIMBimc4SW/zrW4S89BaCA==", + "license": "MIT", + "dependencies": { + "cluster-key-slot": "1.1.2" + }, + "engines": { + "node": ">= 18.19.0" + }, + "peerDependencies": { + "@node-rs/xxhash": "^1.1.0", + "@opentelemetry/api": ">=1 <2" + }, + "peerDependenciesMeta": { + "@node-rs/xxhash": { + "optional": true + }, + "@opentelemetry/api": { + "optional": true + } + } + }, + "node_modules/@redis/json": { + "version": "5.12.1", + "resolved": "https://registry.npmjs.org/@redis/json/-/json-5.12.1.tgz", + "integrity": "sha512-eOze75esLve4vfqDel7aMX08CNaiLLQS2fV8mpRN9NxPe1rVR4vQyYiW/OgtGUysF6QOr9ANhfxABKNOJfXdKg==", + "license": "MIT", + "engines": { + "node": ">= 18.19.0" + }, + "peerDependencies": { + "@redis/client": "^5.12.1" + } + }, + "node_modules/@redis/search": { + "version": "5.12.1", + "resolved": "https://registry.npmjs.org/@redis/search/-/search-5.12.1.tgz", + "integrity": "sha512-ItlxbxC9cKI6IU1TLWoczwJCRb6TdmkEpWv05UrPawqaAnWGRu3rcIqsc5vN483T2fSociuyV1UkWIL5I4//2w==", + "license": "MIT", + "engines": { + "node": ">= 18.19.0" + }, + "peerDependencies": { + "@redis/client": "^5.12.1" + } + }, + "node_modules/@redis/time-series": { + "version": "5.12.1", + "resolved": "https://registry.npmjs.org/@redis/time-series/-/time-series-5.12.1.tgz", + "integrity": "sha512-c6JL6E3EcZJuNqKFz+KM+l9l5mpcQiKvTwgA3blt5glWJ8hjDk0yeHN3beE/MpqYIQ8UEX44ItQzgkE/gCBELQ==", + "license": "MIT", + "engines": { + "node": ">= 18.19.0" + }, + "peerDependencies": { + "@redis/client": "^5.12.1" + } + }, "node_modules/@rollup/plugin-babel": { "version": "5.3.1", "resolved": "https://registry.npmjs.org/@rollup/plugin-babel/-/plugin-babel-5.3.1.tgz", @@ -3601,6 +3675,18 @@ "@types/node": "*" } }, + "node_modules/@types/hoist-non-react-statics": { + "version": "3.3.7", + "resolved": "https://registry.npmjs.org/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.7.tgz", + "integrity": "sha512-PQTyIulDkIDro8P+IHbKCsw7U2xxBYflVzW/FgWdCAePD9xGSidgA76/GeJ6lBKoblyhf9pBY763gbrN+1dI8g==", + "license": "MIT", + "dependencies": { + "hoist-non-react-statics": "^3.3.0" + }, + "peerDependencies": { + "@types/react": "*" + } + }, "node_modules/@types/html-minifier-terser": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/@types/html-minifier-terser/-/html-minifier-terser-6.1.0.tgz", @@ -5613,6 +5699,15 @@ "wrap-ansi": "^7.0.0" } }, + "node_modules/cluster-key-slot": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/cluster-key-slot/-/cluster-key-slot-1.1.2.tgz", + "integrity": "sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/co": { "version": "4.6.0", "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", @@ -8815,6 +8910,21 @@ "he": "bin/he" } }, + "node_modules/hoist-non-react-statics": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz", + "integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==", + "license": "BSD-3-Clause", + "dependencies": { + "react-is": "^16.7.0" + } + }, + "node_modules/hoist-non-react-statics/node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", + "license": "MIT" + }, "node_modules/hoopy": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/hoopy/-/hoopy-0.1.4.tgz", @@ -13698,6 +13808,20 @@ "node": ">=14" } }, + "node_modules/react-cookie": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/react-cookie/-/react-cookie-8.1.0.tgz", + "integrity": "sha512-Qs+gD3gpQmUXnJUZafhJtNWhhNdi8OYbOAF5YQRAZa/D171ILOIEMfXDz/tmhkE+nOthllmqryHH6I/qmvIYWQ==", + "license": "MIT", + "dependencies": { + "@types/hoist-non-react-statics": "^3.3.7", + "hoist-non-react-statics": "^3.3.2", + "universal-cookie": "^8.0.0" + }, + "peerDependencies": { + "react": ">= 16.3.0" + } + }, "node_modules/react-dev-utils": { "version": "12.0.1", "resolved": "https://registry.npmjs.org/react-dev-utils/-/react-dev-utils-12.0.1.tgz", @@ -14004,6 +14128,22 @@ "node": ">=8" } }, + "node_modules/redis": { + "version": "5.12.1", + "resolved": "https://registry.npmjs.org/redis/-/redis-5.12.1.tgz", + "integrity": "sha512-LDsoVvb/CpoV9EN3FXvgvSHNJWuCIzl9MiO3ppOevuGLpSGJhwfQjpEwfFJcQvNSddHADDdZaWx0HnmMxRXG7g==", + "license": "MIT", + "dependencies": { + "@redis/bloom": "5.12.1", + "@redis/client": "5.12.1", + "@redis/json": "5.12.1", + "@redis/search": "5.12.1", + "@redis/time-series": "5.12.1" + }, + "engines": { + "node": ">= 18.19.0" + } + }, "node_modules/reflect.getprototypeof": { "version": "1.0.10", "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz", @@ -16285,6 +16425,28 @@ "node": ">=8" } }, + "node_modules/universal-cookie": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/universal-cookie/-/universal-cookie-8.1.0.tgz", + "integrity": "sha512-65+kikQAWq7gsJbirwB7dk6e8xeug1hx3++x2dQoymdXcV7fYv0yChOgHCg01ZwP3fE3sYeq6EWCSpFv3HLl9g==", + "license": "MIT", + "dependencies": { + "cookie": "^1.1.1" + } + }, + "node_modules/universal-cookie/node_modules/cookie": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz", + "integrity": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/universalify": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", diff --git a/front/package.json b/front/package.json index 234d168..1047457 100644 --- a/front/package.json +++ b/front/package.json @@ -13,9 +13,11 @@ "@types/react-dom": "^19.2.3", "axios": "^1.15.2", "react": "^19.2.5", + "react-cookie": "^8.1.0", "react-dom": "^19.2.5", "react-router": "^7.14.0", "react-scripts": "5.0.1", + "redis": "^5.12.1", "typescript": "^4.9.5", "web-vitals": "^2.1.4" }, diff --git a/front/public/EBM.webp b/front/public/EBM.webp Binary files differnew file mode 100755 index 0000000..6fb0029 --- /dev/null +++ b/front/public/EBM.webp diff --git a/front/src/App.css b/front/src/App.css index dcfa013..d58f45b 100644 --- a/front/src/App.css +++ b/front/src/App.css @@ -19,3 +19,4 @@ padding-left: 10px; padding-right: 10px; } + diff --git a/front/src/App.tsx b/front/src/App.tsx index 2f69086..c106139 100644 --- a/front/src/App.tsx +++ b/front/src/App.tsx @@ -6,11 +6,15 @@ import IndexElement from './Emelents/IndexElement'; import ClassesList from './Emelents/ClassList'; import Topbar from './Emelents/Topbar'; import { AllowedLanguages, LanguageContext } from './Locales/Context'; -import { useState } from 'react'; +import { useEffect, useState } from 'react'; import { ReadCurrentLocale, SaveCurrentLocale } from './Locales/Locales'; import LegalNote from './Emelents/LegalNote'; import WeaponsIndex from './Pages/Weapons'; import RangedWeaponPage from './Pages/RangedWeapon'; +import { AuthContext, Authentication, GetUserInfo } from './Authentication/ContextProvider'; +import { useCookies } from 'react-cookie'; +import LoginPage from './Authentication/LoginPage'; +import { User } from './Authentication/Models'; const router = createBrowserRouter([ { @@ -29,6 +33,10 @@ const router = createBrowserRouter([ path: "/weapons/:id", element: (<RangedWeaponPage />), }, + { + path: "/login", + element: (<LoginPage />), + }, ]); function App() { @@ -40,20 +48,28 @@ function App() { setLang(newLang); } + const [authCookie, ] = useCookies(['X-AUTH-TOKEN']); + const [auth, setAuth] = useState<User | null>(null); + useEffect(() => { + GetUserInfo(authCookie['X-AUTH-TOKEN']).then(v => setAuth(v)); + }, []); + return ( - <LanguageContext.Provider value={lang}> - <div className='App'> - <Topbar /> - <div className='AppContents'> - <Sidebar - setLang={SetLanguage} - /> - <RouterProvider router={router} /> + <AuthContext.Provider value={auth}> + <LanguageContext.Provider value={lang}> + <div className='App'> + <Topbar /> + <div className='AppContents'> + <Sidebar + setLang={SetLanguage} + /> + <RouterProvider router={router} /> + </div> + <LegalNote /> </div> - <LegalNote /> - </div> - </LanguageContext.Provider> + </LanguageContext.Provider> + </AuthContext.Provider> ); } -export default App; +export default App; diff --git a/front/src/Authentication/ContextProvider.ts b/front/src/Authentication/ContextProvider.ts new file mode 100644 index 0000000..040d22e --- /dev/null +++ b/front/src/Authentication/ContextProvider.ts @@ -0,0 +1,38 @@ +import axios from "axios" +import { createContext } from "react" +import { BackendURL } from "../Config" +import { User } from "./Models" + +export type Authentication = { + User: User + Token: string +} + +const UserInfoURL = `${BackendURL}/auth`; + +export const AuthContext = createContext<User | null>(null); + +export function SaveState(data: Authentication | null, setCookie: (cookie: string) => void) { + if (data == null) return; + setCookie(data.Token); +} + +export async function GetUserInfo(displayToken: string): Promise<User | null> { + if (!displayToken || displayToken.length == 0) return null; + try { + const { data, status } = await axios.get<User>( + UserInfoURL, + { + headers: { + Accept: "application/json", + Authorization: `Bearer ${displayToken}` + } + } + ); + if (status != 200) return null; + return data; + } catch (err) { + console.log(`Failed to get user info: ${err}`); + return null; + } +} diff --git a/front/src/Authentication/LoginPage.tsx b/front/src/Authentication/LoginPage.tsx new file mode 100644 index 0000000..e98c506 --- /dev/null +++ b/front/src/Authentication/LoginPage.tsx @@ -0,0 +1,60 @@ +import axios from "axios"; +import { useContext, useState } from "react"; +import { useNavigate } from "react-router"; +import { BackendURL } from "../Config"; +import { Authentication, SaveState } from "./ContextProvider"; +import { useCookies } from 'react-cookie'; +import { GetLocalizedString } from "../Locales/Locales"; +import { LanguageContext } from "../Locales/Context"; + +const LoginURL = `${BackendURL}/auth/login`; + +function LoginPage() { + const lang = useContext(LanguageContext); + + const [username, setUsername] = useState<string>(""); + const [passw, setPassw] = useState<string>(""); + const navigate = useNavigate(); + + function SetAuthState(newAuthState: Authentication | null) { + if (newAuthState) { + console.log(`Logging in as ${newAuthState.User}...`); + } else { + console.log(`Logging out...`); + } + SaveState(newAuthState, (cookie: string) => { + document.cookie = `X-AUTH-TOKEN=${cookie}; path=/;`; + }) + } + + return ( + <div> + <h2> { GetLocalizedString("Username", lang) } </h2> + <input onChange={ev => setUsername(ev.target.value)} /> + <h2> { GetLocalizedString("Password", lang) } </h2> + <input onChange={ev => setPassw(ev.target.value)} /> + <button onClick={() => { + Login(username, passw, (data) => { + SetAuthState(data); + navigate("/"); + window.location.reload(); + }); + }}> { GetLocalizedString("Log in", lang) } </button> + </div> + ); +} + +async function Login(username: string, passw: string, onSuccess: (data: Authentication) => void) { + await axios.post<Authentication>( + LoginURL, { + Username: username, + Password: passw + } + ).then(resp => { + onSuccess(resp.data); + }).catch(err => { + console.log(`Failed to send a login responce: ${err}`); + }); +} + +export default LoginPage; diff --git a/front/src/Authentication/Models.ts b/front/src/Authentication/Models.ts new file mode 100644 index 0000000..1918b85 --- /dev/null +++ b/front/src/Authentication/Models.ts @@ -0,0 +1,4 @@ +export type User = { + Username: string, + Role: "user" | "editor" +} diff --git a/front/src/Emelents/Elements.css b/front/src/Emelents/Elements.css index 6f23c13..7c8c15f 100644 --- a/front/src/Emelents/Elements.css +++ b/front/src/Emelents/Elements.css @@ -21,9 +21,16 @@ } .Topbar > h1 { + margin-top: 0px; + flex-direction: row; margin-bottom: 0px; } +.Topbar > h1 > img { + height: 1em; + width: auto; +} + .TopbarContents { border-top: 2px solid var(--colorscheme-black); display: flex; diff --git a/front/src/Emelents/Sidebar.tsx b/front/src/Emelents/Sidebar.tsx index c650145..ad565fa 100644 --- a/front/src/Emelents/Sidebar.tsx +++ b/front/src/Emelents/Sidebar.tsx @@ -19,6 +19,7 @@ function Sidebar({setLang}: SidebarProps) { <SidebarListElement Href='/' Text='Home' /> <SidebarListElement Href='/classes' Text='Classes' /> <SidebarListElement Href='/weapons' Text='Weapons' /> + <SidebarListElement Href='/login' Text='Login' /> </ul> <button onClick={() => { setLang("ru"); diff --git a/front/src/Emelents/Topbar.tsx b/front/src/Emelents/Topbar.tsx index 73e0e38..284cbb9 100644 --- a/front/src/Emelents/Topbar.tsx +++ b/front/src/Emelents/Topbar.tsx @@ -1,4 +1,5 @@ import { useContext } from "react"; +import { AuthContext } from "../Authentication/ContextProvider"; import { LanguageContext } from "../Locales/Context"; import { GetLocalizedString } from "../Locales/Locales"; @@ -6,11 +7,20 @@ const defaultPathName = 'index'; function Topbar() { var language = useContext(LanguageContext); + var user = useContext(AuthContext); let path = getTopbarElement(window.location.pathname); + function getTopText() { + if (user == null) { + return <h1> View from the edge </h1>; + } else { + return <h1> <img src={"/EBM.webp"} /> { `${GetLocalizedString("Welcome", language)}, ${user.Username}` } </h1>; + } + } + return ( <div className="Topbar"> - <h1> View from the edge </h1> + { getTopText() } <div className="TopbarContents"> <h2> {GetLocalizedString(path, language)} </h2> </div> diff --git a/front/src/Locales/ru_RU.ts b/front/src/Locales/ru_RU.ts index 7802055..f10ac32 100644 --- a/front/src/Locales/ru_RU.ts +++ b/front/src/Locales/ru_RU.ts @@ -8,9 +8,15 @@ lang.LocalizedStrings = new Map<string, string>([ ["contents", "Содержание"], ["classes list", "Список классов"], ["weapons", "Оружие"], + ["login", "Вход"], ["weapons index", "Список вооружения"], ["ranged weapons", "Дальнобойное оружие"], + ["username", "Имя пользователя"], + ["password", "Пароль"], + ["log in", "Войти"], + ["welcome", "Добро пожаловать"], + ["heavy pistols", "Тяжёлые пистолеты"], ["medium pistols", "Средние пистолеты"], @@ -45,6 +51,7 @@ lang.LocalizedStrings = new Map<string, string>([ // Origins ["corebook", "Книга игрока"], + ]); export default lang; diff --git a/front/src/index.tsx b/front/src/index.tsx index 032464f..33c9e94 100644 --- a/front/src/index.tsx +++ b/front/src/index.tsx @@ -3,14 +3,18 @@ import ReactDOM from 'react-dom/client'; import './index.css'; import App from './App'; import reportWebVitals from './reportWebVitals'; +import { CookiesProvider } from 'react-cookie'; const root = ReactDOM.createRoot( document.getElementById('root') as HTMLElement ); + root.render( - <React.StrictMode> - <App /> - </React.StrictMode> + <CookiesProvider> + <React.StrictMode> + <App /> + </React.StrictMode> + </CookiesProvider> ); // If you want to start measuring performance in your app, pass a function |
