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 {
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 './App.css';
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 { 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';
@ -111,7 +111,7 @@ function App() {
} else {
setFoodChoiceList(undefined);
}
}, [choiceRef.current?.value])
}, [choiceRef.current?.value, food])
const doAddChoice = async (event: React.ChangeEvent<HTMLSelectElement>) => {
const index = Object.values(Locations).indexOf(event.target.value as unknown as Locations);
@ -257,8 +257,13 @@ function App() {
<Alert variant={'primary'}>
Poslední změny:
<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>"Blue checkmark" pro uživatele přihlášené přes AD</li>
</ul>
</Alert>
<h1 className='title'>Dnes je {data.date}</h1>
@ -300,11 +305,16 @@ function App() {
<td className='p-0'>
<Table>
<tbody>
{locationLoginList.map((entry: [string, number[]], index) => {
{locationLoginList.map((entry: [string, FoodChoices], index) => {
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}>
<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 === auth.login && <FontAwesomeIcon onClick={() => {
doRemoveChoices(locationKey);
@ -312,7 +322,7 @@ function App() {
</td>
{userChoices?.length && food ? <td className='w-100'>
<ul>
{userChoices.map(foodIndex => {
{userChoices?.map(foodIndex => {
const locationsKey = Object.keys(Locations)[Number(locationKey)]
const restaurantKey = Object.keys(Restaurants).indexOf(locationsKey);
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.
*
* @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
*/
export function generateToken(login?: string): string {
export function generateToken(login?: string, trusted?: boolean): string {
if (!process.env.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) {
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);
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 { getMenuSladovnicka, getMenuTechTower, getMenuUMotliku } from "./restaurants";
import { getQr } from "./qr";
import { generateToken, getLogin, verify } from "./auth";
import { generateToken, getLogin, getTrusted, verify } from "./auth";
import { Locations, Restaurants } from "../../types";
const ENVIRONMENT = process.env.NODE_ENV || 'production';
@ -62,7 +62,7 @@ app.post("/api/login", (req, res) => {
// Autentizace pomocí trusted headers
const remoteUser = req.header('remote-user');
if (remoteUser && remoteUser.length > 0) {
res.status(200).json(generateToken(remoteUser));
res.status(200).json(generateToken(remoteUser, true));
return;
}
// Klasická autentizace loginem
@ -70,7 +70,7 @@ app.post("/api/login", (req, res) => {
throw Error("Nebyl předán login");
}
// 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
@ -207,8 +207,9 @@ app.post("/api/finishDelivery", (req, res) => {
app.post("/api/addChoice", (req, res) => {
const login = getLogin(parseToken(req));
const trusted = getTrusted(parseToken(req));
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);
res.status(200).json(data);
}

View File

@ -249,6 +249,7 @@ export function initIfNeeded() {
export function removeChoices(login: string, location: Locations) {
const today = formatDate(getToday());
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 (login in data.choices[location]) {
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) {
const today = formatDate(getToday());
let data: ClientData = db.get(today);
// TODO řešit ověření uživatele
if (location in data.choices) {
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) {
data.choices[location][login].splice(index, 1)
data.choices[location][login].options.splice(index, 1)
db.set(today, data);
}
}
@ -309,24 +311,41 @@ function removeChoiceIfPresent(login: string) {
* @param login login uživatele
* @param location vybrané "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
*/
export function addChoice(login: string, location: Locations, foodIndex?: number) {
export function addChoice(login: string, trusted: boolean, location: Locations, foodIndex?: number) {
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í
if (foodIndex == null) {
removeChoiceIfPresent(login);
}
const today = formatDate(getToday());
let data: ClientData = db.get(today);
if (!(location in data.choices)) {
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)) {
data.choices[location][login].push(foodIndex);
if (foodIndex != null && !data.choices[location][login].options.includes(foodIndex)) {
data.choices[location][login].options.push(foodIndex);
}
db.set(today, data);
return data;

View File

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