Merge pull request #7 from turniere/ticket/TURNIERE-94

Block sites for unregistered users
This commit is contained in:
Jonny 2019-04-09 08:34:02 +02:00 committed by GitHub
commit ebbff7b01a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 297 additions and 159 deletions

1
.gitignore vendored
View File

@ -1,3 +1,4 @@
/.next/ /.next/
/node_modules/ /node_modules/
/.idea /.idea
check-syntax.sh

View File

@ -2,20 +2,27 @@ import {
Badge, Badge,
Button, Button,
ButtonGroup, ButtonGroup,
Card,
CardBody,
Container,
Collapse, Collapse,
Form,
FormGroup,
Nav, Nav,
Navbar, Navbar,
NavbarBrand, NavbarBrand,
NavbarToggler, NavbarToggler,
NavItem, NavItem,
NavLink NavLink,
Input,
Label
} from 'reactstrap'; } from 'reactstrap';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import React from 'react'; import React from 'react';
import { logout } from './api'; import { login, logout } from './api';
export function BigImage(props) { export function BigImage(props) {
return ( return (
@ -95,7 +102,6 @@ class InvisibleLoginLogoutButtons extends React.Component {
); );
} }
} }
} }
const mapStateToLoginLogoutButtonProperties = (state) => { const mapStateToLoginLogoutButtonProperties = (state) => {
@ -124,3 +130,153 @@ export function Footer() {
</footer> </footer>
); );
} }
/**
* This component works just like a switch statement, although the conditions of the first items
* are checked first, and the first component with a condition that is true will be shown.
*
* For single conditions and options any kind of component can be taken, while the Option-component
* is dedicated for this job. The only important thing is that this component has to have a condition property.
*
* You should also give a default option with a condition that always evaluates to true.
*
* A quick example would be some content that is only to be shown when the user is logged in:
*
* function SomeRestrictedContent(props) {
* const { isSignedIn } = props;
*
* return (
* <UserRestrictor>
* <Option condition={isSignedIn}>
* < The restricted content >
* </Option>
* <Option condition={true}>
* < Some default content; in this case some kind of login >
* </Option>
* </UserRestrictor>
* );
* }
*
* In the example you'll have to note that the default option is at the bottom of all the options
* since it would always be taken otherwise (the options' conditions are checked from top to bottom)
*/
export class UserRestrictor extends React.Component {
render() {
const { children } = this.props;
for(var i in children) {
var c = children[i];
if(c.props.condition) {
return c;
}
}
return null;
}
}
export function Option(props) {
return props.children;
}
export function Login(props) {
return (
<Container className="py-5">
<Card className="shadow">
<CardBody>
<h1 className="custom-font">Login</h1>
<Hint hint={props.hint}/>
<LoginForm/>
<div className="mt-3">
<a href="/register" className="mr-3">Account anlegen</a>
<a href="/register#account-requirement">Warum ist ein Account nötig?</a>
</div>
</CardBody>
</Card>
</Container>
);
}
class LoginErrorList extends React.Component {
render() {
const { error, errorMessages } = this.props;
if(error) {
return (
<ul>
{ errorMessages.map((message, index) =>
<li key={index}>
<style jsx>{`
li {
color:red;
}
`}</style>
{message}
</li>
) }
</ul>
);
} else {
return null;
}
}
}
const mapStateToErrorMessages = (state) => {
const { errorMessages, error } = state.userinfo;
return { errorMessages, error };
};
const VisibleLoginErrorList = connect(
mapStateToErrorMessages
)(LoginErrorList);
class LoginForm extends React.Component {
constructor(props) {
super(props);
this.state = {
email : '',
password : ''
};
}
render() {
return (
<Form>
<FormGroup>
<Label for="username">E-Mail-Adresse</Label>
<Input type="email" name="username" value={this.state.email} onChange={ this.handleEmailInput.bind(this) } />
</FormGroup>
<FormGroup>
<Label for="password">Passwort</Label>
<Input type="password" name="password" value={this.state.password} onChange={ this.handlePasswordInput.bind(this) } />
</FormGroup>
<Button onClick={login.bind(this, this.state.email, this.state.password)} color="success" size="lg" className="w-100 shadow-sm">Anmelden</Button>
<VisibleLoginErrorList/>
</Form>
);
}
handlePasswordInput(input) {
this.setState({ password : input.target.value });
}
handleEmailInput(input) {
this.setState({ email : input.target.value });
}
}
function Hint(props) {
if(props.hint != null) {
return (
<h3>{ props.hint }</h3>
);
} else {
return null;
}
}

View File

@ -57,6 +57,7 @@ const defaultstate_tournamentinfo = {
description : '', description : '',
id : -1, id : -1,
name : '', name : '',
ownerUsername : '',
isPublic : '', isPublic : '',
stages: [], stages: [],
teams : [] teams : []
@ -272,6 +273,7 @@ const reducer_tournamentinfo = (state = defaultstate_tournamentinfo, action) =>
description : action.parameters.description, description : action.parameters.description,
id : action.parameters.id, id : action.parameters.id,
name : action.parameters.name, name : action.parameters.name,
ownerUsername : action.parameters.owner_username,
isPublic : action.parameters.public, isPublic : action.parameters.public,
stages: action.parameters.stages, stages: action.parameters.stages,
teams : action.parameters.teams teams : action.parameters.teams

View File

@ -26,7 +26,7 @@ export class ErrorPageComponent extends React.Component {
} }
} }
function ErrorPage(props){ export function ErrorPage(props){
return ( return (
<Container className="mb-5"> <Container className="mb-5">
<div className="row mb-5"> <div className="row mb-5">

View File

@ -1,7 +1,8 @@
import Head from 'next/head'; import Head from 'next/head';
import '../static/everypage.css'; import '../static/everypage.css';
import { Footer, TurniereNavigation } from '../js/CommonComponents'; import { Footer, TurniereNavigation, UserRestrictor, Option, Login } from '../js/CommonComponents';
import React from 'react'; import React from 'react';
import { connect } from 'react-redux';
import { import {
Button, Button,
@ -22,28 +23,56 @@ import {
import EditableStringList from '../js/EditableStringList'; import EditableStringList from '../js/EditableStringList';
export default class CreatePage extends React.Component { class PrivateCreatePage extends React.Component {
componentDidMount() { componentDidMount() {
verifyCredentials(); verifyCredentials();
} }
render() { render() {
const { isSignedIn } = this.props;
return ( return (
<div className="main generic-fullpage-bg"> <UserRestrictor>
<Head> <Option condition={isSignedIn}>
<title>Turnier erstellen: turnie.re</title> <div className="main generic-fullpage-bg">
</Head> <Head>
<TurniereNavigation/> <title>Turnier erstellen: turnie.re</title>
<div> </Head>
<CreateTournamentCard/> <TurniereNavigation/>
</div> <div>
<Footer/> <CreateTournamentCard/>
</div> </div>
<Footer/>
</div>
</Option>
<Option condition={true}>
<div className="main generic-fullpage-bg">
<Head>
<title>Anmeldung</title>
</Head>
<TurniereNavigation/>
<div>
<Login hint="Sie müssen angemeldet sein, um diesen Inhalt anzuzeigen!"/>
</div>
<Footer/>
</div>
</Option>
</UserRestrictor>
); );
} }
} }
function mapStateToCreatePageProperties(state) {
const { isSignedIn } = state.userinfo;
return { isSignedIn };
}
const CreatePage = connect(
mapStateToCreatePageProperties
)(PrivateCreatePage);
export default CreatePage;
function CreateTournamentCard() { function CreateTournamentCard() {
return ( return (

View File

@ -1,10 +1,7 @@
import Head from 'next/head'; import Head from 'next/head';
import '../static/everypage.css'; import '../static/everypage.css';
import {Footer, TurniereNavigation} from '../js/CommonComponents'; import { Footer, TurniereNavigation, Login } from '../js/CommonComponents';
import React from 'react'; import React from 'react';
import { Button, Card, CardBody, Container, Form, FormGroup, Input, Label } from 'reactstrap';
import { login } from '../js/api';
import { connect } from 'react-redux';
import { import {
verifyCredentials verifyCredentials
@ -31,92 +28,3 @@ export default class LoginPage extends React.Component {
); );
} }
} }
function Login() {
return (
<Container className="py-5">
<Card className="shadow">
<CardBody>
<h1 className="custom-font">Login</h1>
<LoginForm/>
<div className="mt-3">
<a href="/register" className="mr-3">Account anlegen</a>
<a href="/register#account-requirement">Warum ist ein Account nötig?</a>
</div>
</CardBody>
</Card>
</Container>
);
}
class LoginErrorList extends React.Component {
render() {
const { error, errorMessages } = this.props;
if(error) {
return (
<ul>
{ errorMessages.map((message, index) =>
<li key={index}>
<style jsx>{`
li {
color:red;
}
`}</style>
{message}
</li>
) }
</ul>
);
} else {
return null;
}
}
}
const mapStateToErrorMessages = (state) => {
const { errorMessages, error } = state.userinfo;
return { errorMessages, error };
};
const VisibleLoginErrorList = connect(
mapStateToErrorMessages
)(LoginErrorList);
class LoginForm extends React.Component {
constructor(props) {
super(props);
this.state = {
email : '',
password : ''
};
}
render() {
return (
<Form>
<FormGroup>
<Label for="username">E-Mail-Adresse</Label>
<Input type="email" name="username" value={this.state.email} onChange={ this.handleEmailInput.bind(this) } />
</FormGroup>
<FormGroup>
<Label for="password">Passwort</Label>
<Input type="password" name="password" value={this.state.password} onChange={ this.handlePasswordInput.bind(this) } />
</FormGroup>
<Button onClick={login.bind(this, this.state.email, this.state.password)} color="success" size="lg" className="w-100 shadow-sm">Anmelden</Button>
<VisibleLoginErrorList/>
</Form>
);
}
handlePasswordInput(input) {
this.setState({ password : input.target.value });
}
handleEmailInput(input) {
this.setState({ email : input.target.value });
}
}

View File

@ -5,7 +5,7 @@ import { connect } from 'react-redux';
import { notify } from 'react-notify-toast'; import { notify } from 'react-notify-toast';
import { requestTournament } from '../js/api'; import { requestTournament } from '../js/api';
import { BigImage, Footer, TurniereNavigation } from '../js/CommonComponents.js'; import { BigImage, Footer, TurniereNavigation, Login, UserRestrictor, Option } from '../js/CommonComponents.js';
import { ErrorPageComponent } from '../js/components/ErrorComponents.js'; import { ErrorPageComponent } from '../js/components/ErrorComponents.js';
import { import {
@ -34,7 +34,7 @@ class EditTournamentPage extends React.Component {
super(props); super(props);
this.state = { this.state = {
validCode: true validCode: false
}; };
} }
@ -42,7 +42,10 @@ class EditTournamentPage extends React.Component {
verifyCredentials(); verifyCredentials();
requestTournament(this.props.query.code, () => { requestTournament(this.props.query.code, () => {
this.setState({ validCode: true }); this.setState({ validCode: true });
this._edittournamentcontent.notifyOfContentUpdate();
if(this._edittournamentcontent != null) {
this._edittournamentcontent.notifyOfContentUpdate();
}
}, () => { }, () => {
this.setState({ validCode: false }); this.setState({ validCode: false });
}); });
@ -50,33 +53,50 @@ class EditTournamentPage extends React.Component {
render() { render() {
const { validCode } = this.state; const { validCode } = this.state;
const { name } = this.props; const { tournamentname, ownerUsername, isSignedIn, username } = this.props;
if(validCode) { return (
return ( <UserRestrictor>
<div className='pb-5'> <Option condition={ validCode && isSignedIn && ownerUsername === username }>
<Head> <div className='pb-5'>
<title>Turnie.re - Turnier bearbeiten</title> <Head>
</Head> <title>Turnie.re - Turnier bearbeiten</title>
<TurniereNavigation/> </Head>
<BigImage text={ name }/> <TurniereNavigation/>
<EditTournamentContent ref={(edittournamentcontent) => { this._edittournamentcontent = edittournamentcontent; }}/> <BigImage text={ tournamentname }/>
<EditTournamentContent ref={(edittournamentcontent) => { this._edittournamentcontent = edittournamentcontent; }}/>
<Footer/>
</div>
</Option>
<Option condition={ validCode && isSignedIn }>
<ErrorPageComponent statusCode={ 403 }/>
</Option>
<Option condition={ !isSignedIn }>
<div className="main generic-fullpage-bg">
<Head>
<title>Turnie.re - Turnier bearbeiten</title>
</Head>
<TurniereNavigation/>
<Footer/> <div>
</div> <Login hint="Sie müssen angemeldet sein, um ein Turnier zu bearbeiten."/>
); </div>
} else { <Footer/>
return ( </div>
<ErrorPageComponent statusCode={ 404 }/> </Option>
); <Option condition={true}>
} <ErrorPageComponent statusCode={ 404 }/>
</Option>
</UserRestrictor>
);
} }
} }
function mapStateToTournamentInfo(state) { function mapStateToTournamentInfo(state) {
const { name } = state.tournamentinfo; const { tournamentname, ownerUsername } = state.tournamentinfo;
return { name }; const { isSignedIn, username } = state.userinfo;
return { tournamentname, ownerUsername, isSignedIn, username };
} }
export default connect( export default connect(

View File

@ -18,10 +18,12 @@ import {
Row, Row,
Table Table
} from 'reactstrap'; } from 'reactstrap';
import { ErrorPageComponent } from '../js/components/ErrorComponents.js';
import 'bootstrap/dist/css/bootstrap.min.css'; import 'bootstrap/dist/css/bootstrap.min.css';
import {BigImage, Footer, TurniereNavigation} from '../js/CommonComponents.js'; import {BigImage, Footer, TurniereNavigation} from '../js/CommonComponents.js';
import '../static/everypage.css'; import '../static/everypage.css';
import '../static/css/tournament.css'; import '../static/css/tournament.css';
import { connect } from 'react-redux';
import { import {
getRequest, getRequest,
@ -38,7 +40,7 @@ class TournamentPage extends React.Component {
return ( return (
<div className='pb-5'> <div className='pb-5'>
<Container> <Container>
<a href={'/t/' + id + '/edit'} className='btn btn-outline-secondary'>Turnier bearbeiten</a> <EditButton id={id} ownerName={ownerUsername}/>
<p>{description}</p> <p>{description}</p>
<ListGroup> <ListGroup>
<ListGroupItem> <ListGroupItem>
@ -57,6 +59,27 @@ class TournamentPage extends React.Component {
} }
} }
function PrivateEditButton(props) {
const { id, ownerName, isSignedIn, username } = props;
if(isSignedIn && ownerName === username) {
return (
<a href={'/t/' + id + '/edit'} className='btn btn-outline-secondary'>Turnier bearbeiten</a>
);
} else {
return null;
}
}
function mapStateToEditButtonProperties(state) {
const { isSignedIn, username } = state.userinfo;
return { isSignedIn, username };
}
const EditButton = connect(
mapStateToEditButtonProperties
)(PrivateEditButton);
function getLevelName(levelNumber) { function getLevelName(levelNumber) {
const names = ['Finale', 'Halbfinale', 'Viertelfinale', 'Achtelfinale']; const names = ['Finale', 'Halbfinale', 'Viertelfinale', 'Achtelfinale'];
if(levelNumber < names.length){ if(levelNumber < names.length){
@ -66,16 +89,6 @@ function getLevelName(levelNumber) {
} }
} }
function TournamentContainer(props) {
const { tournament } = props.data;
if (tournament === null) {
return <Container>null</Container>;
} else {
return <TournamentPage tournament={tournament}/>;
}
}
function Stage(props) { function Stage(props) {
return (<div> return (<div>
<Container className='py-5'> <Container className='py-5'>
@ -371,25 +384,34 @@ class Main extends React.Component {
getRequest(getState(), '/tournaments/' + code) getRequest(getState(), '/tournaments/' + code)
.then(response => { .then(response => {
this.setState({tournament: convertTournament(response.data)}); this.setState({ status : response.status, tournament : convertTournament(response.data)});
}) })
.catch(() => { /* TODO: Show some kind of error or smth */ }); .catch((err) => {
this.setState({ status : err.response.status });
});
} }
render() { render() {
const tournamentName = this.state.tournament === null ? 'Turnier' : this.state.tournament.name; const tournamentName = this.state.tournament === null ? 'Turnier' : this.state.tournament.name;
return (
<div> const { status, tournament } = this.state;
<Head>
<title>{tournamentName}: turnie.re</title> if (status == 200) {
</Head> return (
<TurniereNavigation/> <div>
<BigImage text={tournamentName}/> <Head>
<TournamentContainer data={this.state}/> <title>{tournamentName}: turnie.re</title>
<Footer/> </Head>
</div> <TurniereNavigation/>
); <BigImage text={tournamentName}/>
<TournamentPage tournament={tournament}/>
<Footer/>
</div>
);
} else {
return <ErrorPageComponent code={status}/>;
}
} }
} }