Skip to content

Commit

Permalink
Add admin area
Browse files Browse the repository at this point in the history
  • Loading branch information
HSZemi committed Sep 4, 2022
1 parent 5488e33 commit fb52eac
Show file tree
Hide file tree
Showing 17 changed files with 743 additions and 3 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ recentDrafts.json
serverState.json
sessions.json
users.json
presets-and-drafts.json
*.log
*.log.xz
*.log.gz
Expand Down
51 changes: 51 additions & 0 deletions create_draft_index.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
#! /usr/bin/env python3

from datetime import datetime, timezone
import sys
import json
from pathlib import Path

def main():
number_of_days = 1
try:
number_of_days = int(sys.argv[1])
except Exception:
pass

info = {'presets':[], 'drafts':{}, 'timestamp': {'presets':'', 'drafts':''}}
output_json = Path(__file__).with_name('presets-and-drafts.json')
if output_json.exists():
info = json.loads(output_json.read_text())

known_draft_ids = set()
for title in info['drafts']:
for item in info['drafts'][title]:
known_draft_ids.add(item['code'])
drafts_dir = Path(__file__).parent / 'data'
now = datetime.now(tz=timezone.utc)
info['timestamp']['drafts'] = str(now)
limit = now.timestamp() - (60*60*24*number_of_days)

for subdir in drafts_dir.glob('*'):
for f in subdir.glob('*.json'):
mtime = f.stat().st_mtime
if mtime > limit:
try:
data = json.loads(f.read_text())
draft_id = f.stem
title = data['preset']['name']
host = data['nameHost']
guest = data['nameGuest']
if title not in info['drafts']:
info['drafts'][title] = []
if draft_id not in known_draft_ids:
info['drafts'][title].append({'code': draft_id, 'host':host, 'guest':guest, 'created':mtime})
known_draft_ids.add(draft_id)
except json.decoder.JSONDecodeError:
print(f'Not a valid json file: {f}')
for title in info['drafts']:
info['drafts'][title] = sorted(info['drafts'][title], reverse=True, key=lambda x: x['created'])
output_json.write_text(json.dumps(info, sort_keys=True))

if __name__ == '__main__':
main()
42 changes: 42 additions & 0 deletions create_preset_index.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
#! /usr/bin/env python3

from datetime import datetime, timezone
import sys
import json
from pathlib import Path

def main():
number_of_days = 1
try:
number_of_days = int(sys.argv[1])
except Exception:
pass

info = {'presets':[], 'drafts':{}, 'timestamp': {'presets':'', 'drafts':''}}
output_json = Path(__file__).with_name('presets-and-drafts.json')
if output_json.exists():
info = json.loads(output_json.read_text())

known_preset_ids = set(item['code'] for item in info['presets'])
presets_dir = Path(__file__).parent / 'presets'
now = datetime.now(tz=timezone.utc)
info['timestamp']['presets'] = str(now)
limit = now.timestamp() - (60*60*24*number_of_days)

for f in presets_dir.glob('*.json'):
mtime = f.stat().st_mtime
if mtime > limit:
try:
data = json.loads(f.read_text())
preset_id = f.stem
name = data['name']
if preset_id not in known_preset_ids:
info['presets'].append({'code':preset_id, 'name':name, 'created':mtime})
known_preset_ids.add(preset_id)
except json.decoder.JSONDecodeError:
print(f'Not a valid json file: {f}')
info['presets'] = sorted(info['presets'], key=lambda x: (x['name'], x['created']))
output_json.write_text(json.dumps(info, sort_keys=True))

if __name__ == '__main__':
main()
7 changes: 7 additions & 0 deletions src/DraftServer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -294,6 +294,13 @@ export class DraftServer {
}
res.json(state);
});
app.get('/api/presets-and-drafts', (req, res) => {
if (!Util.isAuthenticatedRequest(req, sessionStore)) {
res.status(403).end();
return;
}
res.sendFile('presets-and-drafts.json', {'root': __dirname + '/..'});
});
app.post('/api/reload-archive', (req, res) => {
if (!Util.isAuthenticatedRequest(req, sessionStore)) {
res.status(403).end();
Expand Down
30 changes: 29 additions & 1 deletion src/actions/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,16 @@ export interface ISetLanguage {
language: string
}

export interface ISetApiKey {
type: Actions.SET_API_KEY,
apiKey?: string
}

export interface ISetAdminPresetName {
type: Actions.SET_ADMIN_PRESET_NAME,
name?: string
}

export interface ISetIconStyle {
type: Actions.SET_ICON_STYLE,
iconStyle: string
Expand Down Expand Up @@ -207,6 +217,9 @@ export type DraftOwnPropertiesAction = IApplyConfig

export type LanguageAction = ISetLanguage;

export type AdminAction = ISetApiKey
| ISetAdminPresetName;

export type IconStyleAction = ISetIconStyle;

export type ColorSchemeAction = ISetColorScheme;
Expand All @@ -231,7 +244,8 @@ export type Action = DraftAction
| IconStyleAction
| ColorSchemeAction
| ModalAction
| PresetEditorAction;
| PresetEditorAction
| AdminAction;

export function connectPlayer(player: Player, value: string): IConnectPlayer {
return {
Expand Down Expand Up @@ -324,6 +338,20 @@ export function setLanguage(language: string): ISetLanguage {
}
}

export function setApiKey(apiKey: string | undefined): ISetApiKey {
return {
apiKey,
type: Actions.SET_API_KEY
}
}

export function setAdminPresetName(name: string | undefined): ISetAdminPresetName {
return {
name,
type: Actions.SET_ADMIN_PRESET_NAME
}
}

export function setIconStyle(iconStyle: string): ISetIconStyle {
return {
iconStyle,
Expand Down
216 changes: 216 additions & 0 deletions src/components/admin/AdminMain.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,216 @@
import * as React from "react";
import {Dispatch} from "redux";
import * as actions from "../../actions";
import {connect} from "react-redux";
import {ApplicationState, IPresetAndDraftList, IServerState} from "../../types";
import {withTranslation, WithTranslation} from "react-i18next";
import {Redirect, RouteComponentProps} from "react-router";
import PresetsAndDrafts from "./PresetsAndDrafts";

interface Props extends WithTranslation, RouteComponentProps<any> {
apiKey: string | undefined;
onSetApiKey: (apiKey: string | undefined) => void;
}

interface State {
serverState: IServerState
presetsAndDrafts?: IPresetAndDraftList
}

class AdminMain extends React.Component<Props, State> {

constructor(props: Props) {
super(props);
this.state = {
serverState: {maintenanceMode: false, hiddenPresetIds: []},
presetsAndDrafts: undefined,
};
}

private logout() {
this.props.onSetApiKey(undefined);
}

private fetchState() {
fetch('/api/state', {
headers: {
'X-Auth-Token': this.props.apiKey as string
}
})
.then((result) => {
if (result.ok) {
return result.json();
}
this.props.onSetApiKey(undefined);
return Promise.reject('Authorization failed');
})
.then((json) => this.setState({...this.state, serverState: json}));
}

private fetchPresetsAndDrafts() {
fetch('/api/presets-and-drafts', {
headers: {
'X-Auth-Token': this.props.apiKey as string
}
})
.then((result) => {
if (result.ok) {
return result.json();
}
this.props.onSetApiKey(undefined);
return Promise.reject('Authorization failed');
})
.then((json) => this.setState({...this.state, presetsAndDrafts: json}));
}

private toggleMaintenanceMode() {
fetch('/api/state', {
headers: {
'Content-Type': 'application/json; charset=UTF-8',
'X-Auth-Token': this.props.apiKey as string,
},
body: JSON.stringify({maintenanceMode: !this.state.serverState.maintenanceMode}),
method: 'post'
})
.then((result) => {
if (result.ok) {
return result.json();
}
this.props.onSetApiKey(undefined);
return Promise.reject('Authorization failed');
})
.then((json) => this.setState({...this.state, serverState: json}));
}

private saveHiddenPresetIds() {
fetch('/api/state', {
headers: {
'Content-Type': 'application/json; charset=UTF-8',
'X-Auth-Token': this.props.apiKey as string,
},
body: JSON.stringify({hiddenPresetIds: this.state.serverState.hiddenPresetIds}),
method: 'post'
})
.then((result) => {
if (result.ok) {
return result.json();
}
this.props.onSetApiKey(undefined);
return Promise.reject('Authorization failed');
})
.then((json) => this.setState({...this.state, serverState: json}));
}

private reloadDraftArchive() {
fetch('/api/reload-archive', {
headers: {
'Content-Type': 'application/json; charset=UTF-8',
'X-Auth-Token': this.props.apiKey as string,
},
body: JSON.stringify({hiddenPresetIds: this.state.serverState.hiddenPresetIds}),
method: 'post'
})
.then((result) => {
if (result.ok) {
return result.text();
}
this.props.onSetApiKey(undefined);
return Promise.reject('Authorization failed');
})
.then((text) => alert(text));
}

componentDidMount() {
this.fetchState();
this.fetchPresetsAndDrafts();
}

public render() {

if (!this.props.apiKey) {
return (<Redirect to={'/admin/login'}/>);
}

return (
<div className={'content box'}>
<h2>Admin Main</h2>
<button className={'button'} onClick={() => this.logout()}>Logout</button>

<h3>Presets and Drafts</h3>

<PresetsAndDrafts data={this.state.presetsAndDrafts}/>

<hr/>

<h3>Maintenance Mode</h3>

<p>
If maintenance mode is active, no drafts can be created.
Use this to e. g. create a window for an upgrade that does not disrupt ongoing drafts.<br/>
<b>Do not forget to disable maintenance mode afterwards!</b>
</p>

<p className="control">
<input id="toggleMaintenance" type="checkbox" name="toggleMaintenance"
className="switch is-small is-rounded is-info"
checked={this.state.serverState.maintenanceMode} onChange={() => {
this.toggleMaintenanceMode()
}}/>
<label htmlFor="toggleMaintenance">Maintenance Mode
is {this.state.serverState.maintenanceMode ? 'active' : 'off'}</label>
</p>


<h3>Hidden Preset IDs</h3>

<p>
Drafts for hidden Preset IDs are not added to the list of ongoing and recent drafts.<br/>
When a Preset ID is added to this list, existing drafts are not removed from the list of ongoing and
recent drafts.<br/>
When a Preset ID is removed from this list, existing drafts are not added to that list
retroactively.<br/>
Write one Preset ID per line in the textarea below.
</p>

<textarea className="textarea" placeholder="abcdef" id="hiddenPresetIds"
value={this.state.serverState.hiddenPresetIds.join('\n')}
onChange={(event) => {
this.setState({
...this.state,
serverState: {
maintenanceMode: this.state.serverState.maintenanceMode,
hiddenPresetIds: event.target.value.split('\n')
}
});
}}
></textarea>
<button className={'button'} onClick={() => this.saveHiddenPresetIds()}>Save hidden Preset IDs</button>


<h3>Reload Draft Archive</h3>

<p>
After moving stored drafts around on the server, the draft archive has to be reloaded so that the
files can be found again.
</p>

<button className={'button'} onClick={() => this.reloadDraftArchive()}>Reload Draft Archive</button>

</div>
);
}
}

export function mapStateToProps(state: ApplicationState) {
return {
apiKey: state.admin.apiKey,
}
}

export function mapDispatchToProps(dispatch: Dispatch<actions.Action>) {
return {
onSetApiKey: (apiKey: string | undefined) => dispatch(actions.setApiKey(apiKey)),
}
}

export default withTranslation()(connect(mapStateToProps, mapDispatchToProps)(AdminMain));
Loading

0 comments on commit fb52eac

Please sign in to comment.