summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--backend/src/API/AuthenticationAPI.zig8
-rw-r--r--front/package-lock.json162
-rw-r--r--front/package.json2
-rwxr-xr-xfront/public/EBM.webpbin0 -> 9976 bytes
-rw-r--r--front/src/App.css1
-rw-r--r--front/src/App.tsx42
-rw-r--r--front/src/Authentication/ContextProvider.ts38
-rw-r--r--front/src/Authentication/LoginPage.tsx60
-rw-r--r--front/src/Authentication/Models.ts4
-rw-r--r--front/src/Emelents/Elements.css7
-rw-r--r--front/src/Emelents/Sidebar.tsx1
-rw-r--r--front/src/Emelents/Topbar.tsx12
-rw-r--r--front/src/Locales/ru_RU.ts7
-rw-r--r--front/src/index.tsx10
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
new file mode 100755
index 0000000..6fb0029
--- /dev/null
+++ b/front/public/EBM.webp
Binary files differ
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