Základ zobrazování ověřených uživatelů

This commit is contained in:
Martin Berka 2023-07-30 23:36:18 +02:00
parent 028186c8ea
commit 8a75c98c9a
6 changed files with 79 additions and 22 deletions

View File

@ -99,3 +99,8 @@
.select-search-container { .select-search-container {
margin: auto; margin: auto;
} }
.trusted-icon {
color: rgb(0, 89, 255);
margin-right: 10px;
}

View File

@ -12,9 +12,9 @@ import SelectSearch, { SelectedOptionValue } from 'react-select-search';
import 'react-select-search/style.css'; import 'react-select-search/style.css';
import './App.css'; import './App.css';
import { SelectSearchOption } from 'react-select-search'; import { SelectSearchOption } from 'react-select-search';
import { faTrashCan } from '@fortawesome/free-regular-svg-icons'; import { faCircleCheck, faTrashCan } from '@fortawesome/free-regular-svg-icons';
import { useBank } from './context/bank'; import { useBank } from './context/bank';
import { ClientData, Restaurants, Food, Pizza, Order, Locations, PizzaOrder, PizzaDayState } from './types'; import { ClientData, Restaurants, Food, Pizza, Order, Locations, PizzaOrder, PizzaDayState, FoodChoices } from './types';
import Footer from './components/Footer'; import Footer from './components/Footer';
@ -111,7 +111,7 @@ function App() {
} else { } else {
setFoodChoiceList(undefined); setFoodChoiceList(undefined);
} }
}, [choiceRef.current?.value]) }, [choiceRef.current?.value, food])
const doAddChoice = async (event: React.ChangeEvent<HTMLSelectElement>) => { const doAddChoice = async (event: React.ChangeEvent<HTMLSelectElement>) => {
const index = Object.values(Locations).indexOf(event.target.value as unknown as Locations); const index = Object.values(Locations).indexOf(event.target.value as unknown as Locations);
@ -257,8 +257,13 @@ function App() {
<Alert variant={'primary'}> <Alert variant={'primary'}>
Poslední změny: Poslední změny:
<ul> <ul>
<li>(Trochu) přehlednější zobrazení tabulky</li> <li>(Trochu) přehlednější zobrazení tabulky
<ul>
<li>Je to pořád ošklivý :(</li>
</ul>
</li>
<li>(Opět) možnost vybrat jen jednu variantu</li> <li>(Opět) možnost vybrat jen jednu variantu</li>
<li>"Blue checkmark" pro uživatele přihlášené přes AD</li>
</ul> </ul>
</Alert> </Alert>
<h1 className='title'>Dnes je {data.date}</h1> <h1 className='title'>Dnes je {data.date}</h1>
@ -300,11 +305,16 @@ function App() {
<td className='p-0'> <td className='p-0'>
<Table> <Table>
<tbody> <tbody>
{locationLoginList.map((entry: [string, number[]], index) => { {locationLoginList.map((entry: [string, FoodChoices], index) => {
const login = entry[0]; const login = entry[0];
const userChoices = entry[1]; const userPayload = entry[1];
const userChoices = userPayload?.options;
const trusted = userPayload?.trusted || false;
return <tr key={index}> return <tr key={index}>
<td className='text-nowrap'> <td className='text-nowrap'>
{trusted && <span className='trusted-icon'>
<FontAwesomeIcon title='Uživatel ověřený doménovým přihlášením' icon={faCircleCheck} style={{ cursor: "help" }} />
</span>}
{login} {login}
{login === auth.login && <FontAwesomeIcon onClick={() => { {login === auth.login && <FontAwesomeIcon onClick={() => {
doRemoveChoices(locationKey); doRemoveChoices(locationKey);
@ -312,7 +322,7 @@ function App() {
</td> </td>
{userChoices?.length && food ? <td className='w-100'> {userChoices?.length && food ? <td className='w-100'>
<ul> <ul>
{userChoices.map(foodIndex => { {userChoices?.map(foodIndex => {
const locationsKey = Object.keys(Locations)[Number(locationKey)] const locationsKey = Object.keys(Locations)[Number(locationKey)]
const restaurantKey = Object.keys(Restaurants).indexOf(locationsKey); const restaurantKey = Object.keys(Restaurants).indexOf(locationsKey);
const restaurant = Object.values(Restaurants)[restaurantKey]; const restaurant = Object.values(Restaurants)[restaurantKey];

View File

@ -4,9 +4,10 @@ import jwt from 'jsonwebtoken';
* Vygeneruje a vrátí podepsaný JWT token pro daný login. * Vygeneruje a vrátí podepsaný JWT token pro daný login.
* *
* @param login přihlašovací jméno uživatele * @param login přihlašovací jméno uživatele
* @param trusted příznak, zda se jedná o ověřeného uživatele
* @returns JWT token * @returns JWT token
*/ */
export function generateToken(login?: string): string { export function generateToken(login?: string, trusted?: boolean): string {
if (!process.env.JWT_SECRET) { if (!process.env.JWT_SECRET) {
throw Error("Není vyplněna proměnná prostředí JWT_SECRET"); throw Error("Není vyplněna proměnná prostředí JWT_SECRET");
} }
@ -16,7 +17,7 @@ export function generateToken(login?: string): string {
if (!login || login.trim().length === 0) { if (!login || login.trim().length === 0) {
throw Error("Nebyl předán login"); throw Error("Nebyl předán login");
} }
return jwt.sign({ login }, process.env.JWT_SECRET); return jwt.sign({ login, trusted: trusted || false }, process.env.JWT_SECRET);
} }
/** /**
@ -51,3 +52,19 @@ export function getLogin(token?: string): string {
const payload: any = jwt.verify(token, process.env.JWT_SECRET); const payload: any = jwt.verify(token, process.env.JWT_SECRET);
return payload.login; return payload.login;
} }
/**
* Vrátí zda je uživatel používající daný token ověřený, pokud je token platný.
*
* @param token JWT token
*/
export function getTrusted(token?: string): boolean {
if (!process.env.JWT_SECRET) {
throw Error("Není vyplněna proměnná prostředí JWT_SECRET");
}
if (!token) {
throw Error("Nebyl předán token");
}
const payload: any = jwt.verify(token, process.env.JWT_SECRET);
return payload.trusted || false;
}

View File

@ -8,7 +8,7 @@ import dotenv from 'dotenv';
import path from 'path'; import path from 'path';
import { getMenuSladovnicka, getMenuTechTower, getMenuUMotliku } from "./restaurants"; import { getMenuSladovnicka, getMenuTechTower, getMenuUMotliku } from "./restaurants";
import { getQr } from "./qr"; import { getQr } from "./qr";
import { generateToken, getLogin, verify } from "./auth"; import { generateToken, getLogin, getTrusted, verify } from "./auth";
import { Locations, Restaurants } from "../../types"; import { Locations, Restaurants } from "../../types";
const ENVIRONMENT = process.env.NODE_ENV || 'production'; const ENVIRONMENT = process.env.NODE_ENV || 'production';
@ -62,7 +62,7 @@ app.post("/api/login", (req, res) => {
// Autentizace pomocí trusted headers // Autentizace pomocí trusted headers
const remoteUser = req.header('remote-user'); const remoteUser = req.header('remote-user');
if (remoteUser && remoteUser.length > 0) { if (remoteUser && remoteUser.length > 0) {
res.status(200).json(generateToken(remoteUser)); res.status(200).json(generateToken(remoteUser, true));
return; return;
} }
// Klasická autentizace loginem // Klasická autentizace loginem
@ -70,7 +70,7 @@ app.post("/api/login", (req, res) => {
throw Error("Nebyl předán login"); throw Error("Nebyl předán login");
} }
// TODO zavést podmínky pro délku loginu (min i max) // TODO zavést podmínky pro délku loginu (min i max)
res.status(200).json(generateToken(req.body.login)); res.status(200).json(generateToken(req.body.login, false));
}); });
// TODO dočasné řešení - QR se zobrazuje přes <img>, nemáme sem jak dostat token // TODO dočasné řešení - QR se zobrazuje přes <img>, nemáme sem jak dostat token
@ -207,8 +207,9 @@ app.post("/api/finishDelivery", (req, res) => {
app.post("/api/addChoice", (req, res) => { app.post("/api/addChoice", (req, res) => {
const login = getLogin(parseToken(req)); const login = getLogin(parseToken(req));
const trusted = getTrusted(parseToken(req));
if (req.body.locationIndex > -1) { if (req.body.locationIndex > -1) {
const data = addChoice(login, req.body.locationIndex, req.body.foodIndex); const data = addChoice(login, trusted, req.body.locationIndex, req.body.foodIndex);
io.emit("message", data); io.emit("message", data);
res.status(200).json(data); res.status(200).json(data);
} }

View File

@ -249,6 +249,7 @@ export function initIfNeeded() {
export function removeChoices(login: string, location: Locations) { export function removeChoices(login: string, location: Locations) {
const today = formatDate(getToday()); const today = formatDate(getToday());
let data: ClientData = db.get(today); let data: ClientData = db.get(today);
// TODO zajistit, že neověřený uživatel se stejným loginem nemůže mazat volby ověřeného
if (location in data.choices) { if (location in data.choices) {
if (login in data.choices[location]) { if (login in data.choices[location]) {
delete data.choices[location][login] delete data.choices[location][login]
@ -273,11 +274,12 @@ export function removeChoices(login: string, location: Locations) {
export function removeChoice(login: string, location: Locations, foodIndex: number) { export function removeChoice(login: string, location: Locations, foodIndex: number) {
const today = formatDate(getToday()); const today = formatDate(getToday());
let data: ClientData = db.get(today); let data: ClientData = db.get(today);
// TODO řešit ověření uživatele
if (location in data.choices) { if (location in data.choices) {
if (login in data.choices[location]) { if (login in data.choices[location]) {
const index = data.choices[location][login].indexOf(foodIndex); const index = data.choices[location][login].options.indexOf(foodIndex);
if (index > -1) { if (index > -1) {
data.choices[location][login].splice(index, 1) data.choices[location][login].options.splice(index, 1)
db.set(today, data); db.set(today, data);
} }
} }
@ -309,24 +311,41 @@ function removeChoiceIfPresent(login: string) {
* @param login login uživatele * @param login login uživatele
* @param location vybrané "umístění" * @param location vybrané "umístění"
* @param foodIndex volitelný index jídla v daném umístění * @param foodIndex volitelný index jídla v daném umístění
* @param trusted příznak, zda se jedná o ověřeného uživatele
* @returns aktuální data * @returns aktuální data
*/ */
export function addChoice(login: string, location: Locations, foodIndex?: number) { export function addChoice(login: string, trusted: boolean, location: Locations, foodIndex?: number) {
initIfNeeded(); initIfNeeded();
const today = formatDate(getToday());
let data: ClientData = db.get(today);
// Ověření, že se neověřený užívatel nepokouší přepsat údaje ověřeného
const locations = Object.values(data?.choices);
let found = false;
if (!trusted) {
for (const location of locations) {
if (Object.keys(location).includes(login) && location[login].trusted) {
found = true;
}
}
}
if (!trusted && found) {
throw Error("Nelze změnit volbu ověřeného uživatele");
}
// Pokud měníme pouze lokaci, mažeme případné předchozí // Pokud měníme pouze lokaci, mažeme případné předchozí
if (foodIndex == null) { if (foodIndex == null) {
removeChoiceIfPresent(login); removeChoiceIfPresent(login);
} }
const today = formatDate(getToday());
let data: ClientData = db.get(today);
if (!(location in data.choices)) { if (!(location in data.choices)) {
data.choices[location] = {}; data.choices[location] = {};
} }
if (!(login in data.choices[location])) { if (!(login in data.choices[location])) {
data.choices[location][login] = []; data.choices[location][login] = {
trusted,
options: []
};
} }
if (foodIndex != null && !data.choices[location][login].includes(foodIndex)) { if (foodIndex != null && !data.choices[location][login].options.includes(foodIndex)) {
data.choices[location][login].push(foodIndex); data.choices[location][login].options.push(foodIndex);
} }
db.set(today, data); db.set(today, data);
return data; return data;

View File

@ -5,9 +5,14 @@ export enum Restaurants {
TECHTOWER = 'techTower', TECHTOWER = 'techTower',
} }
export interface FoodChoices {
trusted: boolean,
options: number[]
}
export interface Choices { export interface Choices {
[location: string]: { [location: string]: {
[login: string]: number[] [login: string]: FoodChoices
}, },
} }