Added: Errormessage for users incl. hints, reading of of current rfid into user-editor
This commit is contained in:
parent
32e6018415
commit
37f134fd13
@ -1,16 +1,15 @@
|
|||||||
const server = 'http://192.168.4.22'
|
const server = 'http://192.168.4.22'
|
||||||
|
async function restoreSession(reducer = null) {
|
||||||
async function restoreSession(reducer=null) {
|
|
||||||
|
|
||||||
let token = localStorage.getItem('token');
|
let token = localStorage.getItem('token');
|
||||||
let username = localStorage.getItem('username');
|
let username = localStorage.getItem('username');
|
||||||
console.log({type:'start', token, username});
|
console.log({ type: 'start', token, username });
|
||||||
let active = token!==null && username!==null && await checkAuth(token);
|
let active = token !== null && username !== null && await checkAuth(token);
|
||||||
if(active&&reducer!==null){
|
if (active && reducer !== null) {
|
||||||
reducer({type:'start', token, username});
|
reducer({ type: 'start', token, username });
|
||||||
console.log({type:'start', token, username});
|
console.log({ type: 'start', token, username });
|
||||||
}
|
}
|
||||||
return (active ? { active: true, token, username } : {active:false});
|
return (active ? { active: true, token, username } : { active: false });
|
||||||
}
|
}
|
||||||
async function storeSession(username, token) {
|
async function storeSession(username, token) {
|
||||||
token = await token;
|
token = await token;
|
||||||
@ -24,7 +23,19 @@ async function login(username, password) {
|
|||||||
formData.append('username', username)
|
formData.append('username', username)
|
||||||
formData.append('password', password);
|
formData.append('password', password);
|
||||||
formData.append('action', 'login');
|
formData.append('action', 'login');
|
||||||
const resp = fetch(`${server}/api/auth`, { method: 'POST', mode: 'cors', body: formData }).then(resp => storeSession(username, resp.text()));
|
const controller = new AbortController();
|
||||||
|
const id = setTimeout(() => controller.abort(), 1500);
|
||||||
|
const resp = fetch(`${server}/api/auth`, { signal: controller.signal, method: 'POST', mode: 'cors', body: formData })
|
||||||
|
.then(resp => {
|
||||||
|
if (resp.ok) return storeSession(username, resp.text())
|
||||||
|
else if(resp.status===401) return 'login_failed';
|
||||||
|
else throw new Error(resp.error);
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.log('Error is:', error);
|
||||||
|
return 'network_error';
|
||||||
|
})
|
||||||
|
;
|
||||||
return resp;
|
return resp;
|
||||||
}
|
}
|
||||||
async function logout(token) {
|
async function logout(token) {
|
||||||
@ -87,8 +98,13 @@ function parsedb(raw) {
|
|||||||
users.push({ line, uid, first_name, last_name, rfid_uid, user_pin, enabled: enabled[0] === '1' });
|
users.push({ line, uid, first_name, last_name, rfid_uid, user_pin, enabled: enabled[0] === '1' });
|
||||||
});
|
});
|
||||||
return users;
|
return users;
|
||||||
|
|
||||||
}
|
}
|
||||||
const publicfunctions = { login, logout, checkAuth, parsedb, fetchdb, createCsvTable, updateUser, deleteUser, createUser,restoreSession };
|
|
||||||
|
async function catchRFID(token){
|
||||||
|
const resp = await fetch(`${server}/api/rfid`, { method: 'GET', mode: 'cors', headers: { Authentification: token } })
|
||||||
|
.then(resp => resp.json());
|
||||||
|
return resp;
|
||||||
|
}
|
||||||
|
const publicfunctions = { login, logout, checkAuth, parsedb, fetchdb, createCsvTable, updateUser, deleteUser, createUser, restoreSession, catchRFID };
|
||||||
export default { ...publicfunctions }
|
export default { ...publicfunctions }
|
||||||
export { login, logout, checkAuth, parsedb, fetchdb, createCsvTable, updateUser, deleteUser, createUser, restoreSession }
|
export { login, logout, checkAuth, parsedb, fetchdb, createCsvTable, updateUser, deleteUser, createUser, restoreSession, catchRFID }
|
@ -1,9 +1,9 @@
|
|||||||
import { h } from 'preact'
|
import { h } from 'preact'
|
||||||
|
|
||||||
function TextBox({ id, type = 'text', label, disabled, formdata, formchange, maxlength }) {
|
function TextBox({ id, type = 'text', label, disabled, formdata, formchange, maxlength, overridevalue }) {
|
||||||
let onChange = (e) => formchange({ ...formdata, [e.target.id]: e.target.value });
|
let onChange = (e) => formchange({ ...formdata, [e.target.id]: e.target.value });
|
||||||
return (<div className={'textbox'} >
|
return (<div className={'textbox'} >
|
||||||
<input placeholder=" " disabled={disabled} type={type} value={formdata[id]?formdata[id]:''} id={id} onInput={onChange} maxlength={maxlength} name={`form-${id}`} />
|
<input placeholder=" " disabled={disabled} type={type} value={overridevalue?overridevalue:formdata[id]?formdata[id]:''} id={id} onInput={onChange} maxlength={maxlength} name={`form-${id}`} />
|
||||||
<label for={id}>{label}</label>
|
<label for={id}>{label}</label>
|
||||||
</div>)
|
</div>)
|
||||||
}
|
}
|
||||||
|
@ -4,6 +4,6 @@ import Pageselector from "./Pageselector";
|
|||||||
import Header from "./header";
|
import Header from "./header";
|
||||||
import UserList from "./userlist";
|
import UserList from "./userlist";
|
||||||
import Menu from "./menu";
|
import Menu from "./menu";
|
||||||
|
import Warnbox from "./warnbox";
|
||||||
export {App, Breadcrumbs, Pageselector, Header, UserList, Menu}
|
export {App, Breadcrumbs, Pageselector, Header, UserList, Menu, Warnbox}
|
||||||
export default App
|
export default App
|
15
src/components/warnbox/index.jsx
Normal file
15
src/components/warnbox/index.jsx
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
function Warnbox({ title = "Fehler", children }) {
|
||||||
|
return (<div className={'warnbox'} >
|
||||||
|
|
||||||
|
<span className='warnbox__icon'>⚠</span>
|
||||||
|
|
||||||
|
<div className="column">
|
||||||
|
<h3>{title}</h3>
|
||||||
|
{children && <span> {children}</span>}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export default Warnbox
|
@ -1,5 +1,5 @@
|
|||||||
import { h } from 'preact';
|
import { h } from 'preact';
|
||||||
import { useContext, useEffect, useState } from 'preact/hooks';
|
import { useContext, useEffect, useState, useCallback } from 'preact/hooks';
|
||||||
import { route } from 'preact-router';
|
import { route } from 'preact-router';
|
||||||
import AppState, { UserTable } from "../../store";
|
import AppState, { UserTable } from "../../store";
|
||||||
import api from '../../api'
|
import api from '../../api'
|
||||||
@ -8,12 +8,28 @@ import { CheckBox, Button, TextBox } from '../../components/controls';
|
|||||||
function EditUser({ userid }) {
|
function EditUser({ userid }) {
|
||||||
let [sessiondata,] = useContext(AppState).session;
|
let [sessiondata,] = useContext(AppState).session;
|
||||||
const { usertable, userreducer } = useContext(UserTable);
|
const { usertable, userreducer } = useContext(UserTable);
|
||||||
|
|
||||||
const [formdata, formchange] = useState({});
|
const [formdata, formchange] = useState({});
|
||||||
|
const [rfidscan, setscan] = useState({active:false, rfidscaninterval:null});
|
||||||
|
let rfidscaninterval = null;
|
||||||
let maxlength_uid = 10;
|
let maxlength_uid = 10;
|
||||||
let maxlength_name = 25;
|
let maxlength_name = 25;
|
||||||
let maxlength_pin = 10;
|
let maxlength_pin = 10;
|
||||||
let maxlength_rfid = 8;
|
let maxlength_rfid = 8;
|
||||||
|
useEffect(() => {
|
||||||
|
if (rfidscan.rfidscaninterval === null && rfidscan.active) {
|
||||||
|
rfidscan.rfidscaninterval = setInterval(() => {
|
||||||
|
api.catchRFID(sessiondata.token).then(result => {
|
||||||
|
if (result.rfid_uid) {
|
||||||
|
formchange(old => ({ ...old, rfid_uid: result.rfid_uid }))
|
||||||
|
setscan(p => ({...p,active:false}))
|
||||||
|
}
|
||||||
|
}).catch(() => { setscan(p => ({...p,active:false})) });
|
||||||
|
}, 1000);
|
||||||
|
} else if (rfidscan.rfidscaninterval !== null && !rfidscan.active) {
|
||||||
|
clearInterval(rfidscan.rfidscaninterval);
|
||||||
|
setscan(p => ({...p,rfidscaninterval:null}));
|
||||||
|
}
|
||||||
|
}, [rfidscan]);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let user = userid !== undefined ? usertable.find(u => u.uid === userid) : undefined;
|
let user = userid !== undefined ? usertable.find(u => u.uid === userid) : undefined;
|
||||||
if (user !== undefined) {
|
if (user !== undefined) {
|
||||||
@ -28,6 +44,16 @@ function EditUser({ userid }) {
|
|||||||
let user = userid !== undefined ? usertable.find(u => u.uid === userid) : undefined;
|
let user = userid !== undefined ? usertable.find(u => u.uid === userid) : undefined;
|
||||||
formchange(user !== undefined ? user : {});
|
formchange(user !== undefined ? user : {});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const runScan = (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
setscan(p => ({...p,active:!p.active}));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
const onSubmit = (e) => {
|
const onSubmit = (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
let idcount = uidCount(usertable, formdata);
|
let idcount = uidCount(usertable, formdata);
|
||||||
@ -49,7 +75,7 @@ function EditUser({ userid }) {
|
|||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<div className='container'>
|
<div className='container'>
|
||||||
<Breadcrumbs items={['Benutzerverwaltung',formdata.line != undefined ? "Benutzer Bearbeiten" : "Neuer Benutzer"]} />
|
<Breadcrumbs items={['Benutzerverwaltung', formdata.line != undefined ? "Benutzer Bearbeiten" : "Neuer Benutzer"]} />
|
||||||
<div className={'contentbox'} >
|
<div className={'contentbox'} >
|
||||||
<h2>{formdata.line != undefined ? "Benutzer Bearbeiten" : "Neuer Benutzer"}</h2>
|
<h2>{formdata.line != undefined ? "Benutzer Bearbeiten" : "Neuer Benutzer"}</h2>
|
||||||
<form onSubmit={onSubmit}>
|
<form onSubmit={onSubmit}>
|
||||||
@ -57,7 +83,7 @@ function EditUser({ userid }) {
|
|||||||
<div className='row'>
|
<div className='row'>
|
||||||
<div className='column'>
|
<div className='column'>
|
||||||
<TextBox info={true} formdata={formdata} formchange={formchange} id="uid" label="Benutzer-ID" maxlength={maxlength_uid} />
|
<TextBox info={true} formdata={formdata} formchange={formchange} id="uid" label="Benutzer-ID" maxlength={maxlength_uid} />
|
||||||
{<span class="textbox__info"> {formdata["uid"]?formdata["uid"].length:0} von {maxlength_uid} Zeichen verwendet.</span>}
|
{<span class="textbox__info"> {formdata["uid"] ? formdata["uid"].length : 0} von {maxlength_uid} Zeichen verwendet.</span>}
|
||||||
</div>
|
</div>
|
||||||
<div className='column'>
|
<div className='column'>
|
||||||
<b>Info</b>
|
<b>Info</b>
|
||||||
@ -69,22 +95,32 @@ function EditUser({ userid }) {
|
|||||||
<div className='row'>
|
<div className='row'>
|
||||||
<div className='column'>
|
<div className='column'>
|
||||||
<TextBox formdata={formdata} formchange={formchange} id="first_name" label="Vorname" maxlength={maxlength_name} />
|
<TextBox formdata={formdata} formchange={formchange} id="first_name" label="Vorname" maxlength={maxlength_name} />
|
||||||
{<span class="textbox__info"> {formdata["first_name"] ?formdata["first_name"].length:0} von {maxlength_name} Zeichen verwendet.</span>}
|
{<span class="textbox__info"> {formdata["first_name"] ? formdata["first_name"].length : 0} von {maxlength_name} Zeichen verwendet.</span>}
|
||||||
</div>
|
</div>
|
||||||
<div className='column'>
|
<div className='column'>
|
||||||
<TextBox formdata={formdata} formchange={formchange} id="last_name" label="NachName" maxlength={maxlength_name} />
|
<TextBox formdata={formdata} formchange={formchange} id="last_name" label="NachName" maxlength={maxlength_name} />
|
||||||
{<span class="textbox__info"> {formdata["last_name"]?formdata["last_name"].length:0} von {maxlength_name} Zeichen verwendet.</span>}
|
{<span class="textbox__info"> {formdata["last_name"] ? formdata["last_name"].length : 0} von {maxlength_name} Zeichen verwendet.</span>}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<h3>Authentifizierung</h3>
|
<h3>Authentifizierung</h3>
|
||||||
<div className='row'>
|
<div className='row'>
|
||||||
<div className='column'>
|
<div className='column'>
|
||||||
<TextBox formdata={formdata} formchange={formchange} id="rfid_uid" label="RFID" maxlength={maxlength_rfid} />
|
<TextBox formdata={formdata} formchange={formchange} id="rfid_uid" label="RFID" maxlength={maxlength_rfid} disabled={rfidscan.active} overridevalue={rfidscan.active&&"Scannen..."}/>
|
||||||
{<span class="textbox__info"> {formdata["rfid_uid"]?formdata["rfid_uid"].length:0} von {maxlength_rfid} Zeichen verwendet.</span>}
|
{<span class="textbox__info"> {formdata["rfid_uid"] ? formdata["rfid_uid"].length : 0} von {maxlength_rfid} Zeichen verwendet.</span>}
|
||||||
</div>
|
</div>
|
||||||
|
<div className='column'>
|
||||||
|
<Button onClick={runScan}>{rfidscan.active ? "Abbrechen" : "Scannen"}</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
<div className='row'>
|
||||||
<div className='column'>
|
<div className='column'>
|
||||||
<TextBox formdata={formdata} formchange={formchange} id="user_pin" label="Pin" maxlength={maxlength_pin} />
|
<TextBox formdata={formdata} formchange={formchange} id="user_pin" label="Pin" maxlength={maxlength_pin} />
|
||||||
{<span class="textbox__info"> {formdata["user_pin"] ?formdata["user_pin"].length:0} von {maxlength_pin} Zeichen verwendet.</span>}
|
{<span class="textbox__info"> {formdata["user_pin"] ? formdata["user_pin"].length : 0} von {maxlength_pin} Zeichen verwendet.</span>}
|
||||||
|
</div>
|
||||||
|
<div className='column'>
|
||||||
|
<b>Info</b>
|
||||||
|
<span>Achten Sie darauf, eine PIN <br />nicht mehrfach zu benutzen.</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<h3>Status</h3>
|
<h3>Status</h3>
|
||||||
|
@ -2,7 +2,7 @@ import { h } from 'preact';
|
|||||||
import { route } from 'preact-router';
|
import { route } from 'preact-router';
|
||||||
import { useContext, useState } from 'preact/hooks';
|
import { useContext, useState } from 'preact/hooks';
|
||||||
import AppState from '../../store/AppState';
|
import AppState from '../../store/AppState';
|
||||||
import Breadcrumbs from '../../components/breadcrumbs';
|
import {Breadcrumbs, Warnbox} from '../../components';
|
||||||
import { CheckBox, Button, TextBox } from '../../components/controls';
|
import { CheckBox, Button, TextBox } from '../../components/controls';
|
||||||
import api from '../../api'
|
import api from '../../api'
|
||||||
function Login() {
|
function Login() {
|
||||||
@ -13,19 +13,23 @@ function Login() {
|
|||||||
route('/', true);
|
route('/', true);
|
||||||
function onSubmit(e) {
|
function onSubmit(e) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
api.login(val.username, val.password).then(token => {
|
api.login(val.username, val.password).then(result => {
|
||||||
if (token != 'failed!') {
|
if(result == 'login_failed'){
|
||||||
|
set(prev=>({ ...prev, error: "login_failed",password: '' }));
|
||||||
|
}
|
||||||
|
else if(result=='network_error')
|
||||||
|
set(prev=>({ ...prev, error: "network_connection" }));
|
||||||
|
else {
|
||||||
|
console.log(typeof(result))
|
||||||
let newsession = {
|
let newsession = {
|
||||||
type: 'start',
|
type: 'start',
|
||||||
username: val.username,
|
username: val.username,
|
||||||
token
|
token: result
|
||||||
}
|
}
|
||||||
setsession(newsession);
|
setsession(newsession);
|
||||||
}
|
|
||||||
else {
|
|
||||||
set(prev=>({ ...prev, error: "user" }));
|
|
||||||
}
|
|
||||||
set({ username: '', password: '' });
|
set({ username: '', password: '' });
|
||||||
|
}
|
||||||
|
|
||||||
})
|
})
|
||||||
|
|
||||||
}
|
}
|
||||||
@ -35,7 +39,9 @@ function Login() {
|
|||||||
<div className={'contentbox'} >
|
<div className={'contentbox'} >
|
||||||
<h2>Anmeldung</h2>
|
<h2>Anmeldung</h2>
|
||||||
<p >Bitte melden Sie sich mit ihren Nutzerdaten an.</p>
|
<p >Bitte melden Sie sich mit ihren Nutzerdaten an.</p>
|
||||||
{val.error !== null && <span style={'color: red'}>Fehler: Ungültige Anmeldedaten.</span>}
|
|
||||||
|
{val.error ==='login_failed' && <Warnbox title='Anmeldefehler'>Ungültige Anmeldedaten.<br />Bitte überprüfen Sie den eingebenen Benutzernamen und das Passwort.</Warnbox>}
|
||||||
|
{val.error ==='network_connection' && <Warnbox title='Netwerkfehler'>Die Kommunikation mit dem Gerät ist zurzeit nicht möglich.<br />Bitte überprüfen Sie die Netzwerkverbindung.</Warnbox>}
|
||||||
<form id="login_form" onSubmit={onSubmit} >
|
<form id="login_form" onSubmit={onSubmit} >
|
||||||
<div className='row'>
|
<div className='row'>
|
||||||
<TextBox maxlength={25} formdata={val} formchange={set} id="username" label="Benutzername" />
|
<TextBox maxlength={25} formdata={val} formchange={set} id="username" label="Benutzername" />
|
||||||
|
@ -1,19 +1,25 @@
|
|||||||
@mixin default
|
@mixin default
|
||||||
z-index: 1
|
z-index: 1
|
||||||
margin: 1em
|
margin: 0
|
||||||
padding: 0.5em
|
padding: 0.5em
|
||||||
text-align: center
|
text-align: center
|
||||||
display: inline-block
|
display: block
|
||||||
|
width: auto
|
||||||
position: relative
|
position: relative
|
||||||
color: #333
|
color: #333
|
||||||
font-weight: bold
|
font-weight: bold
|
||||||
text-decoration: none
|
text-decoration: none
|
||||||
border: solid 1px #999
|
border: solid 1px #999
|
||||||
border-radius: .2rem
|
border-radius: .2rem
|
||||||
|
min-width: 100%
|
||||||
|
overflow: hidden
|
||||||
|
&--disabled
|
||||||
|
color: #999
|
||||||
|
|
||||||
button
|
button
|
||||||
display: block
|
display: block
|
||||||
background: none
|
background: none
|
||||||
width: 100%
|
min-width: 100%
|
||||||
border: none
|
border: none
|
||||||
border-radius: none
|
border-radius: none
|
||||||
font-size: 2em
|
font-size: 2em
|
||||||
|
@ -1,9 +1,10 @@
|
|||||||
@mixin textfield
|
@mixin textfield
|
||||||
margin: 0 1em
|
margin: 0 1em
|
||||||
width: 100%
|
min-width: 100%
|
||||||
position: relative
|
position: relative
|
||||||
display: block
|
display: block
|
||||||
margin: 0
|
margin: 0
|
||||||
|
overflow: hidden
|
||||||
background: #fafafa
|
background: #fafafa
|
||||||
border-radius: .3em
|
border-radius: .3em
|
||||||
border-bottom: 1px solid #ccc
|
border-bottom: 1px solid #ccc
|
||||||
@ -12,7 +13,7 @@
|
|||||||
input
|
input
|
||||||
display: block
|
display: block
|
||||||
background: none
|
background: none
|
||||||
width: 100%
|
min-width: 100%
|
||||||
border-radius: none
|
border-radius: none
|
||||||
outline: none
|
outline: none
|
||||||
border: none
|
border: none
|
||||||
@ -23,19 +24,24 @@
|
|||||||
position: absolute
|
position: absolute
|
||||||
top: 0.15em
|
top: 0.15em
|
||||||
left: 0.25em
|
left: 0.25em
|
||||||
|
right: 0
|
||||||
font-size: .7em
|
font-size: .7em
|
||||||
cursor: text
|
cursor: text
|
||||||
transition: 250ms all
|
transition: 250ms all
|
||||||
|
text-overflow: ellipsis
|
||||||
|
|
||||||
&::placeholder
|
&::placeholder
|
||||||
color: transparent
|
color: transparent
|
||||||
|
|
||||||
&:placeholder-shown + label
|
&:placeholder-shown + label
|
||||||
|
text-overflow: ellipsis
|
||||||
color: grey
|
color: grey
|
||||||
position: absolute
|
position: absolute
|
||||||
padding: .7em
|
padding: .7em
|
||||||
top: 0em
|
top: 0em
|
||||||
left: 0em
|
left: 0em
|
||||||
|
right: 0
|
||||||
|
bottom: 0
|
||||||
font-size: 1em
|
font-size: 1em
|
||||||
&__info
|
&__info
|
||||||
color: #ccc
|
color: #ccc
|
||||||
|
31
src/style/_warnbox.sass
Normal file
31
src/style/_warnbox.sass
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
.warnbox
|
||||||
|
display: flex
|
||||||
|
flex-direction: row
|
||||||
|
//flex-wrap: wrap
|
||||||
|
background: #ffc964
|
||||||
|
box-shadow: 0 0 1em #ffc964
|
||||||
|
border-radius: .3em
|
||||||
|
border: 1px solid #fff
|
||||||
|
align-items: stretch
|
||||||
|
justify-content: stretch
|
||||||
|
padding: .3em
|
||||||
|
&__icon
|
||||||
|
font-size: 2em
|
||||||
|
line-height: 0.5em
|
||||||
|
//width: 2em
|
||||||
|
border-right: 1px solid #fff
|
||||||
|
color: #fff
|
||||||
|
font-weight: bold
|
||||||
|
text-shadow: 0 0 .2em #fff
|
||||||
|
padding: .25em
|
||||||
|
margin: 0
|
||||||
|
display: flex
|
||||||
|
justify-content: center
|
||||||
|
align-items: center
|
||||||
|
//align-self: flex-start
|
||||||
|
h3
|
||||||
|
margin: .3em
|
||||||
|
color: #fff
|
||||||
|
div span
|
||||||
|
margin-left: 1em
|
||||||
|
|
@ -6,6 +6,7 @@
|
|||||||
@use 'footer'
|
@use 'footer'
|
||||||
@use 'button'
|
@use 'button'
|
||||||
@use 'input'
|
@use 'input'
|
||||||
|
@import 'warnbox'
|
||||||
*
|
*
|
||||||
//border: 1px dotted red
|
//border: 1px dotted red
|
||||||
box-sizing: border-box
|
box-sizing: border-box
|
||||||
|
Loading…
x
Reference in New Issue
Block a user