├── .env.example ├── .gitignore ├── .meta └── tudelft.svg ├── CHANGELOG.md ├── LOGUI-ROOT ├── README.md ├── app └── logui-client │ ├── app.js │ ├── applications │ ├── add.js │ ├── landing.js │ └── menu.js │ ├── client.js │ ├── common │ ├── about.js │ ├── conferenceProceedings.js │ ├── footer.js │ ├── header.js │ ├── landing.js │ ├── loadingSplash.js │ ├── logUIDevice.js │ └── notFound.js │ ├── constants.js │ ├── flight │ ├── add.js │ ├── landing.js │ └── token.js │ ├── nav │ ├── menu │ │ ├── blank.js │ │ └── menu.js │ └── trail │ │ ├── trail.js │ │ └── trailItem.js │ ├── session │ └── landing.js │ ├── settings │ └── landing.js │ └── user │ ├── landing.js │ ├── login.js │ ├── logout.js │ └── menu.js ├── docker-compose.yaml ├── proxy ├── Dockerfile ├── nginx.conf └── responses │ ├── 404.html │ ├── 500.html │ └── 502.html ├── requirements ├── development.txt └── requirements.txt ├── scripts ├── create_env.ps1 ├── create_env.sh ├── create_user.bat ├── create_user.sh ├── dev_db.sh └── dev_server.sh ├── static └── logui │ ├── css │ ├── error.css │ └── normalize.css │ ├── fonts │ ├── roboto-300-l.woff2 │ ├── roboto-300-le.woff2 │ ├── roboto-400-l.woff2 │ ├── roboto-400-le.woff2 │ ├── roboto-500-l.woff2 │ ├── roboto-500-le.woff2 │ ├── roboto-700-l.woff2 │ └── roboto-700-le.woff2 │ ├── img │ ├── favicon.svg │ ├── icons.svg │ ├── logo.svg │ └── tudelft-logo.svg │ └── less │ ├── about.less │ ├── add.less │ ├── applications.less │ ├── core.less │ ├── flight.less │ ├── footer.less │ ├── global.less │ ├── header.less │ ├── icons.less │ ├── loadingSplash.less │ ├── login.less │ ├── main.less │ ├── nav │ ├── grid.less │ ├── menu.less │ └── trail.less │ ├── roboto.less │ └── session.less └── worker ├── .babelrc ├── Dockerfile ├── http-entrypoint.sh ├── logui_apps ├── __init__.py ├── control │ ├── __init__.py │ ├── admin.py │ ├── apps.py │ ├── migrations │ │ └── __init__.py │ ├── models.py │ ├── templates │ │ └── logui │ │ │ └── control │ │ │ └── landing.html │ ├── tests.py │ ├── urls.py │ └── views.py ├── control_api │ ├── __init__.py │ ├── application │ │ ├── __init__.py │ │ ├── serializers.py │ │ └── views.py │ ├── flight │ │ ├── __init__.py │ │ ├── serializers.py │ │ └── views.py │ ├── migrations │ │ └── __init__.py │ ├── serializers.py │ ├── session │ │ ├── __init__.py │ │ ├── serializers.py │ │ └── views.py │ ├── urls.py │ ├── user │ │ ├── __init__.py │ │ ├── serializers.py │ │ └── views.py │ └── utils.py ├── errorhandling │ ├── __init__.py │ └── views.py └── websocket │ ├── __init__.py │ ├── admin.py │ ├── apps.py │ ├── consumers │ ├── __init__.py │ ├── endpoint.py │ └── interface.py │ ├── migrations │ └── __init__.py │ ├── models.py │ ├── routing.py │ ├── tests.py │ └── views.py ├── manage.py ├── mongo.py ├── package-lock.json ├── package.json ├── templates └── logui │ ├── base.html │ ├── errors │ ├── 404.html │ └── 500.html │ └── nojs.html └── worker ├── __init__.py ├── asgi.py ├── settings ├── __init__.py ├── base.py ├── development.py └── docker.py ├── urls.py └── wsgi.py /.env.example: -------------------------------------------------------------------------------- 1 | PROXY_LISTEN_ON=8000 2 | DOCKER_HOSTNAME=<> 3 | DEBUG=False 4 | SECRET_KEY=<> 5 | DATABASE_PASSWORD=<> -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # System databases 2 | Thumbs.db 3 | .DS_Store 4 | 5 | # Python Specifics 6 | *.pyc 7 | __pycache__/ 8 | 9 | # VSCode 10 | .vscode/* 11 | !.vscode/settings.json 12 | !.vscode/tasks.json 13 | !.vscode/launch.json 14 | !.vscode/extensions.json 15 | 16 | # Repository-specific rules 17 | db.sqlite3 18 | .env 19 | **/migrations/* 20 | !**/migrations/__init__.py 21 | **/node_modules/ 22 | /static/cache/ 23 | /worker/collectedstatic/ -------------------------------------------------------------------------------- /.meta/tudelft.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 9 | 10 | 13 | 17 | 19 | 21 | 30 | 34 | 38 | 41 | 43 | 46 | 47 | 49 | 55 | 57 | 59 | 61 | 62 | 63 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # LogUI Server Changelog 2 | 3 | This Markdown file contains the `CHANGELOG` for LogUI server. Changes are made as per the [GNU CHANGELOG Style Guide](https://www.gnu.org/prep/standards/html_node/Style-of-Change-Logs.html). 4 | 5 | ``` 6 | 7 | 2021-01-15 Version 0.4.0 8 | 9 | Implemented basic repository structure, including working build. 10 | 11 | * React application in progress -- login functionality added. 12 | * Codebase working strictly in a development environment only; not meant for release. 13 | 14 | 2021-03-26 Version 0.5.0 15 | 16 | Works with LogUI client version 0.5.0. 17 | 18 | Basic functionality implemented. Repository has been hevily reworked since the 0.4.0 release. Version 0.5.0 is the first version we're happy to let people try out. Hopefully nothing will break. 19 | 20 | * Updated Python environment requirements, and npm for the JavaScript side of things. 21 | * Added React application for management of the LogUI server instance (the control application). 22 | * Functional Dockerised environment, complete with scripts to instantiate the .env file (along with Windows PowerShell and batch variants). 23 | * Use of MongoDB to capture interaction data. 24 | * Basic Django applications and data models to handle capturing and management of data. 25 | * Functional WebSocket server to handle incoming requests from the LogUI client. 26 | * Functional basic authorisation via use of an encrypted string. 27 | 28 | 2021-03-26 Version 0.5.1 29 | 30 | Works with LogUI client version 0.5.1 and above. 31 | 32 | Altered the configuration object to include an authorisation token, not an authentication token. Tidying up terminology. 33 | 34 | 2021-03-31 Version 0.5.2 35 | 36 | Updated the logic dictating version checks on clients connecting to the server. 37 | We now use a 'startswith' check, rather than an absolute match. 38 | ``` -------------------------------------------------------------------------------- /LOGUI-ROOT: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/logui-framework/server/b6635943865a71f8b9e2dcc2aaafabe5bbd7ff83/LOGUI-ROOT -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # LogUI Server 2 | 3 | **Welcome to LogUI!** *LogUI* is a powerful, framework-agnostic client-side JavaScript library that can be used for logging interactions that take place on a webpage. Primarily designed for *Interactive Information Retrieval (IIR)* experiments, LogUI can in theory be used on any page or site that you wish to track fine-grained user interactions with UI components. 4 | 5 | This repository houses the LogUI server component. It's a Dockerised, containerised service that you can spin up on a computer with the appropriate version of Docker installed. LogUI server works in tandem with the LogUI client library, to be used as part of any web application or website that you wish to track interactions on. You can find the LogUI client at [this repository](https://github.com/logui-framework/client/). 6 | 7 | ## About LogUI 8 | 9 | The LogUI library is implemented by [Dr David Maxwell](https://github.com/maxwelld90/), a postdoctoral researcher at [TUDelft](https://www.tudelft.nl/) in the Netherlands. It has been developed in the Lambda Lab, headed by [Dr Claudia Hauff](https://chauff.github.io/). The library is borne out of the need for infrastructure that allows one to undertake the logging of user interactions in a consistent way, rather than the piecemeal approach that we've seen in IIR experimentation. 10 | 11 | We think that a one-size-fits-all logging library is just the ticket for your experiments! 12 | 13 | ## Using LogUI in Experiments? 14 | 15 | We're thrilled that you're using LogUI in your experiments! We ask that in return you provide due credit for this work. If you have a paper associated with your experiment, please do cite the associated demonstration paper that was published at [ECIR 2021](https://www.ecir2021.eu/). You can find the BibTeX source for the paper below. 16 | 17 | ```bibtex 18 | @inproceedings{maxwell2021logui, 19 | author = {Maxwell, David and Hauff, Claudia}, 20 | title ="{LogUI: Contemporary Logging Infrastructure for Web-Based Experiments}", 21 | booktitle = {Advances in Information Retrieval (Proc. ECIR)}, 22 | year = {2021}, 23 | pages = {525--530}, 24 | } 25 | ``` 26 | 27 | ## Documentation and First Run Guide 28 | 29 | For documentation on LogUI server, please go and check out the [corresponding Wiki](https://github.com/logui-framework/server/wiki/) associated with this repository. As an example, you'll find an in-depth [first run guide](https://github.com/logui-framework/server/wiki/First-Run-Guide), showing you exactly what you need to do to get LogUI server running on your computer of choice. More detailed information about the specifics of LogUI server, and what you can do with it, are also available on the Wiki. 30 | 31 | ## Found a Bug or have a Feature Request? 32 | 33 | It would be great to hear from you! Please [raise an issue in this repository](https://github.com/logui-framework/server/issues) and we can discuss what options that can be pursued to resolve it. -------------------------------------------------------------------------------- /app/logui-client/app.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import regeneratorRuntime from 'regenerator-runtime'; // Include this for async methods/functions to work 4 | 5 | import LogUIClientApp from './client'; 6 | 7 | ReactDOM.render( 8 | 9 | 10 | , 11 | document.getElementById('approot') 12 | ); -------------------------------------------------------------------------------- /app/logui-client/applications/add.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Menu from '../applications/menu'; 3 | import TrailItem from '../nav/trail/trailItem'; 4 | import LogUIDevice from '../common/logUIDevice'; 5 | import Constants from '../constants'; 6 | import {withRouter, Redirect} from 'react-router-dom'; 7 | 8 | class ApplicationAddPage extends React.Component { 9 | 10 | constructor(props) { 11 | super(props); 12 | 13 | this.state = { 14 | applicationName: '', 15 | formState: 0, 16 | }; 17 | 18 | this.processForm = this.processForm.bind(this); 19 | } 20 | 21 | getTrail() { 22 | return [ 23 | , 24 | , 25 | , 26 | ]; 27 | } 28 | 29 | async componentDidMount() { 30 | this.props.clientMethods.setMenuComponent(Menu); 31 | this.props.clientMethods.setTrailComponent(this.getTrail()); 32 | this.newApplicationNameField.focus(); 33 | } 34 | 35 | async checkApplicationName(currentName) { 36 | var response = await fetch(`${Constants.SERVER_API_ROOT}application/add/check/?name=${currentName}`, { 37 | method: 'GET', 38 | headers: { 39 | 'Authorization': `jwt ${this.props.clientMethods.getLoginDetails().token}` 40 | }, 41 | }); 42 | 43 | await response.json().then(data => { 44 | this.handleNameCheckResponse(data); 45 | }); 46 | } 47 | 48 | handleNameChange(value) { 49 | this.checkApplicationName(value); 50 | 51 | this.setState({ 52 | applicationName: value, 53 | }); 54 | } 55 | 56 | handleNameCheckResponse(data) { 57 | if (!data.is_available) { 58 | this.setState({ 59 | formState: 2, 60 | }); 61 | 62 | return; 63 | } 64 | 65 | if (this.state.formState == 2) { 66 | this.setState({ 67 | formState: 1, 68 | }); 69 | } 70 | } 71 | 72 | formFieldFocusBlur(isFocus) { 73 | if (this.state.formState == 2) { 74 | return; 75 | } 76 | 77 | this.setState({ 78 | formState: isFocus ? 1 : 0, 79 | }); 80 | } 81 | 82 | processForm(e) { 83 | e.preventDefault(); 84 | 85 | if (this.state.formState == 2 || this.state.formState == 3) { 86 | return; 87 | } 88 | 89 | var response = fetch(`${Constants.SERVER_API_ROOT}application/add/`, { 90 | method: 'POST', 91 | headers: { 92 | 'Content-Type': 'application/json', 93 | 'Authorization': `jwt ${this.props.clientMethods.getLoginDetails().token}`, 94 | }, 95 | body: JSON.stringify({ 96 | name: this.state.applicationName, 97 | }), 98 | }).then(e => { 99 | switch (e.status) { 100 | case 201: 101 | this.props.clientMethods.setMenuShouldUpdate(true); 102 | 103 | this.setState({ 104 | formState: 4, 105 | }); 106 | break; 107 | case 409: 108 | this.setState({ 109 | formState: 2, 110 | }); 111 | break; 112 | default: 113 | this.setState({ 114 | formState: 3, 115 | }); 116 | break; 117 | } 118 | }); 119 | } 120 | 121 | renderRedirect() { 122 | if (this.state.formState == 4) { 123 | return (); 124 | } 125 | } 126 | 127 | render() { 128 | let messageBox = null; 129 | 130 | // 0 is default 131 | // 1 is click on the field 132 | // 2 is when the name is already taken. 133 | // 3 something went wrong on submission. 134 | // 4 success, added. 135 | 136 | switch (this.state.formState) { 137 | case 1: 138 | messageBox = 139 |
140 | Type a new application name into this field. 141 |
; 142 | break; 143 | case 2: 144 | messageBox = 145 |
146 | This application name already exists. Please try another one. 147 |
; 148 | break; 149 | case 3: 150 | messageBox = 151 |
152 | Something went wrong with the submission of the form. Please resubmit, or try again later. 153 |
; 154 | break; 155 | default: 156 | messageBox = 157 |
158 | Enter a new application name into the input field, and click Add New Application. 159 |
; 160 | } 161 | 162 | return( 163 |
164 | {this.renderRedirect()} 165 |
166 |
167 |

Add New Application

168 |
169 | 170 |

171 | Here, you can add a new application to track with . Fill out the form fields below, and click Add New Application to add the new application to the database. 172 |

173 | 174 |
175 | 176 |
177 | 186 |
187 | 188 | 189 |
190 |
191 | 192 | {messageBox} 193 | 194 |
195 |
196 |
197 | ) 198 | }; 199 | 200 | } 201 | 202 | export default ApplicationAddPage; -------------------------------------------------------------------------------- /app/logui-client/applications/landing.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Menu from './menu'; 3 | import Constants from '../constants'; 4 | import TrailItem from '../nav/trail/trailItem'; 5 | import {Link} from 'react-router-dom'; 6 | 7 | class ApplicationsLandingPage extends React.Component { 8 | 9 | constructor(props) { 10 | super(props); 11 | 12 | this.state = { 13 | appListing: [], 14 | }; 15 | } 16 | 17 | getTrail() { 18 | return [ 19 | , 20 | , 21 | ]; 22 | } 23 | 24 | async getListing() { 25 | var response = await fetch(`${Constants.SERVER_API_ROOT}application/list/`, { 26 | method: 'GET', 27 | headers: { 28 | 'Authorization': `jwt ${this.props.clientMethods.getLoginDetails().token}` 29 | }, 30 | }); 31 | 32 | await response.json().then(data => { 33 | this.setState({ 34 | appListing: data, 35 | }); 36 | }); 37 | } 38 | 39 | componentDidMount() { 40 | this.props.clientMethods.setMenuComponent(Menu); 41 | this.props.clientMethods.setTrailComponent(this.getTrail()); 42 | 43 | this.getListing(); 44 | } 45 | 46 | render() { 47 | let appList = this.state.appListing; 48 | let authToken = this.props.clientMethods.getLoginDetails().token; 49 | 50 | return( 51 |
52 |
53 |
54 |

Applications

55 |
    56 |
  • Add New Application
  • 57 |
58 |
59 | 60 |

61 | LogUI lets you create a series of applications to track interactions on. An application could be, for example, your experimental system. 62 |

63 | 64 | {appList.length == 0 ? 65 |

There are no monitored applications in the LogUI database yet. Click here to add a new application.

66 | 67 | : 68 | 69 |
70 |
71 | Application Name 72 | Created At 73 | Flights 74 |
75 | 76 | {Object.keys(appList).map(function(key) { 77 | return ( 78 | 85 | ); 86 | })} 87 |
88 | } 89 |
90 |
91 | ) 92 | }; 93 | 94 | } 95 | 96 | class ApplicationListItem extends React.Component { 97 | 98 | constructor(props) { 99 | super(props); 100 | 101 | this.state = { 102 | isActive: this.props.isActive, 103 | }; 104 | } 105 | 106 | render() { 107 | return( 108 |
109 | 110 | {this.props.name} 111 | {this.props.id} 112 | 113 | 114 | {this.props.timestampSplit.time.locale} 115 | {this.props.timestampSplit.date.friendly} 116 | 117 | {this.props.flights} 118 | View Application 119 |
120 | ); 121 | } 122 | } 123 | 124 | export default ApplicationsLandingPage; -------------------------------------------------------------------------------- /app/logui-client/applications/menu.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Constants from '../constants'; 3 | import {Link} from 'react-router-dom'; 4 | 5 | class Menu extends React.Component { 6 | 7 | constructor(props) { 8 | super(props); 9 | this.state = { 10 | appListing: [], 11 | initialised: false, 12 | } 13 | } 14 | 15 | async getAppListing() { 16 | var response = await fetch(`${Constants.SERVER_API_ROOT}application/list/`, { 17 | method: 'GET', 18 | headers: { 19 | 'Authorization': `jwt ${this.props.clientMethods.getLoginDetails().token}` 20 | }, 21 | }); 22 | 23 | await response.json().then(data => { 24 | this.setState({ 25 | appListing: data, 26 | initialised: true, 27 | }); 28 | 29 | this.props.clientMethods.setMenuShouldUpdate(false); 30 | }); 31 | } 32 | 33 | async componentDidMount() { 34 | this.getAppListing(); 35 | } 36 | 37 | componentDidUpdate() { 38 | this.getAppListing(); 39 | } 40 | 41 | shouldComponentUpdate() { 42 | if (!this.state.initialised || this.props.clientMethods.shouldMenuUpdate()) { 43 | return true; 44 | } 45 | 46 | return false; 47 | } 48 | 49 | render() { 50 | let appList = this.state.appListing; 51 | let appListLength = appList.length; 52 | 53 | let appListRender = ( 54 |
    55 | {Object.keys(appList).map(function(key) { 56 | return ( 57 | 58 | ); 59 | })} 60 |
61 | ); 62 | 63 | return( 64 |
65 |

Applications

66 | 67 | {appListLength > 0 ?

Current Applications

: <>} 68 | {appListLength > 0 ? appListRender : <>} 69 | 70 |

Settings

71 |
    72 |
  • Add New Application
  • 73 |
74 |
75 | ); 76 | } 77 | } 78 | 79 | class MenuItem extends React.Component { 80 | 81 | constructor(props) { 82 | super(props); 83 | } 84 | 85 | render() { 86 | return( 87 |
  • {this.props.name}
  • 88 | ); 89 | } 90 | 91 | } 92 | 93 | export default Menu; -------------------------------------------------------------------------------- /app/logui-client/client.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {HashRouter as Router, Route, Switch} from 'react-router-dom'; 3 | 4 | import HeaderPageComponent from './common/header'; 5 | import TrailPageComponent from './nav/trail/trail'; 6 | import MenuPageComponent from './nav/menu/menu'; 7 | 8 | import LandingPage, {Submenu as LandingPageSubmenu} from './common/landing'; 9 | import ApplicationsLandingPage from './applications/landing'; 10 | import ApplicationsNewPage from './applications/add'; 11 | 12 | import FlightsLandingPage from './flight/landing'; 13 | import FlightAuthorisationTokenPage from './flight/token'; 14 | import FlightAddPage from './flight/add'; 15 | 16 | import ViewSessionPage from './session/landing'; 17 | 18 | import SettingsLandingPage from './settings/landing'; 19 | 20 | import UserLandingPage from './user/landing'; 21 | import UserLoginPage from './user/login'; 22 | import UserLogoutPage from './user/logout'; 23 | 24 | import LoadingSplash from './common/loadingSplash'; 25 | import NotFoundPage from './common/notFound'; 26 | import AboutPage from './common/about'; 27 | 28 | import Constants from './constants'; 29 | 30 | class LogUIClientApp extends React.Component { 31 | 32 | constructor(props) { 33 | super(props); 34 | 35 | this.state = { 36 | hideSplashScreen: false, 37 | currentMenuComponent: null, 38 | currentTrail: null, 39 | isLoggedIn: false, 40 | loginDetails: null, 41 | landingMessage: null, 42 | shouldMenuUpdate: true, 43 | }; 44 | 45 | this.setMenuComponent = this.setMenuComponent.bind(this); 46 | this.setTrailComponent = this.setTrailComponent.bind(this); 47 | this.login = this.login.bind(this); 48 | this.logout = this.logout.bind(this); 49 | this.setLoginDetails = this.setLoginDetails.bind(this); 50 | this.getLoginDetails = this.getLoginDetails.bind(this); 51 | this.getLoginToken = this.getLoginToken.bind(this); 52 | this.setLandingMessage = this.setLandingMessage.bind(this); 53 | this.clearLandingMessage = this.clearLandingMessage.bind(this); 54 | this.shouldMenuUpdate = this.shouldMenuUpdate.bind(this); 55 | this.setMenuShouldUpdate = this.setMenuShouldUpdate.bind(this); 56 | 57 | this.methodReferences = { 58 | setMenuComponent: this.setMenuComponent, 59 | setTrailComponent: this.setTrailComponent, 60 | getLoginDetails: this.getLoginDetails, 61 | login: this.login, 62 | logout: this.logout, 63 | setLandingMessage: this.setLandingMessage, 64 | clearLandingMessage: this.clearLandingMessage, 65 | shouldMenuUpdate: this.shouldMenuUpdate, 66 | setMenuShouldUpdate: this.setMenuShouldUpdate, 67 | } 68 | } 69 | 70 | setMenuComponent(component) { 71 | this.setState({ 72 | currentMenuComponent: component 73 | }); 74 | } 75 | 76 | setTrailComponent(trailArray) { 77 | this.setState({ 78 | currentTrail: trailArray 79 | }) 80 | } 81 | 82 | async login(username, password) { 83 | try { 84 | var response = await fetch(`${Constants.SERVER_API_ROOT}user/auth/`, { 85 | method: 'POST', 86 | headers: { 87 | 'Content-Type': 'application/json', 88 | }, 89 | body: JSON.stringify({ 90 | username: username, 91 | password: password, 92 | }), 93 | }); 94 | } 95 | catch (err) { 96 | console.error("Something went wrong with communicating with the server."); 97 | } 98 | 99 | var responseJSON = null; 100 | await response.json().then(data => responseJSON = data); 101 | 102 | if (response.status != 200) { 103 | return { 104 | loginSuccess: false 105 | } 106 | } 107 | 108 | this.setLoginDetails(responseJSON.token, responseJSON.user); 109 | 110 | this.setState({ 111 | isLoggedIn: true, 112 | }); 113 | 114 | return { 115 | loginSuccess: true 116 | } 117 | } 118 | 119 | logout() { 120 | this.setState({ 121 | isLoggedIn: false, 122 | loginDetails: null, 123 | }); 124 | 125 | this.setLandingMessage(1); 126 | 127 | window.sessionStorage.removeItem(Constants.SESSIONSTORAGE_AUTH_TOKEN); 128 | } 129 | 130 | getLoginDetails() { 131 | return { 132 | token: window.sessionStorage.getItem(Constants.SESSIONSTORAGE_AUTH_TOKEN), 133 | user: this.state.loginDetails, 134 | } 135 | } 136 | 137 | async getLoginToken() { 138 | let token = window.sessionStorage.getItem(Constants.SESSIONSTORAGE_AUTH_TOKEN); 139 | 140 | if (token) { 141 | var response = await fetch(`${Constants.SERVER_API_ROOT}user/current/`, { 142 | method: 'GET', 143 | headers: { 144 | 'Content-Type': 'application/json', 145 | 'Authorization': `jwt ${token}` 146 | }, 147 | }); 148 | 149 | response.json().then(data => { 150 | if (response.status == 200) { 151 | this.setState({ 152 | isLoggedIn: true, 153 | loginDetails: data, 154 | }); 155 | } 156 | }); 157 | } 158 | } 159 | 160 | setLoginDetails(token, userObject) { 161 | this.setState({ 162 | loginDetails: userObject 163 | }); 164 | 165 | this.setLandingMessage(2); 166 | 167 | window.sessionStorage.setItem(Constants.SESSIONSTORAGE_AUTH_TOKEN, token); 168 | } 169 | 170 | componentDidMount() { 171 | this.getLoginToken(); 172 | 173 | setTimeout( 174 | () => { 175 | this.setState({ 176 | hideSplashScreen: true 177 | }) 178 | }, 179 | Constants.SPLASH_PERSIST_DELAY 180 | ); 181 | } 182 | 183 | setLandingMessage(messageID) { 184 | this.setState({ 185 | landingMessage: messageID, 186 | }); 187 | } 188 | 189 | clearLandingMessage() { 190 | this.setState({ 191 | landingMessage: null, 192 | }); 193 | } 194 | 195 | setMenuShouldUpdate(value) { 196 | this.setState({ 197 | shouldMenuUpdate: value, 198 | }); 199 | } 200 | 201 | shouldMenuUpdate() { 202 | return this.state.shouldMenuUpdate; 203 | } 204 | 205 | shouldComponentUpdate(nextProps, nextState) { 206 | if (this.state.landingMessage && nextState && !nextState.landingMessage) { 207 | return false; 208 | } 209 | 210 | return true; 211 | } 212 | 213 | render() { 214 | return( 215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 | ()} 228 | /> 229 | 230 | ()} 236 | /> 237 | 238 | ()} 244 | /> 245 | 246 | ()} 251 | /> 252 | 253 | ()} 258 | /> 259 | 260 | ()} 265 | /> 266 | 267 | ()} 272 | /> 273 | 274 | ()} 280 | /> 281 | 282 | ()} 288 | /> 289 | 290 | ()} 296 | /> 297 | 298 | ()} 304 | /> 305 | 306 | ()} 312 | /> 313 | 314 | ()} 318 | /> 319 | 320 | 321 | ) 322 | }; 323 | 324 | } 325 | 326 | export default LogUIClientApp; -------------------------------------------------------------------------------- /app/logui-client/common/about.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Constants from '../constants'; 3 | import TrailItem from '../nav/trail/trailItem'; 4 | import Footer from '../common/footer'; 5 | import ConferenceProceedings from '../common/conferenceProceedings'; 6 | import LogUIDevice from '../common/logUIDevice'; 7 | 8 | class AboutPage extends React.Component { 9 | 10 | getTrail() { 11 | return [ 12 | , 13 | , 14 | ]; 15 | } 16 | 17 | componentDidMount() { 18 | this.props.clientMethods.setMenuComponent(null); 19 | this.props.clientMethods.setTrailComponent(this.getTrail()); 20 | } 21 | 22 | render() { 23 | return ( 24 |
    25 |
    26 |

    About LogUI

    27 |

    is a contemporary logging library for capturing user interactions in web-based experiments. Designed with the Information Retrieval community in mind, can be applied anywhere fine-grained logging is required in a web environment. You can find more information about on our GitHub page.

    28 | 29 |

    Who Wrote ?

    30 |

    is a project of the Lambda Lab at Delft University of Technology in The Netherlands. Development is led by Dr David Maxwell, a postdoctoral researcher. Associate Professor Dr Claudia Hauff is the PI of the Lambda Lab, where we look at Information Retrieval research; specifically, Search as Learning.

    31 |

    We developed with funding from the Nederlandse Organisatie voor Wetenschappelijk Onderzoek (NWO) 🇳🇱, under projects SearchX (639.022.722) and Aspasia (015.013.027).

    32 | 33 |

    Third Party Credits

    34 |

    Icons used within are taken from the Noun Project.

    35 | 36 |

    Using in Your Research?

    37 |

    Are you using to capture data for your experiments? We're thrilled that you are! In return for using our software, we kindly ask that you cite our demonstration paper in your work when discussing your methodology. The BibTeX code is available in the box below.

    38 | 39 | 40 | 41 |
    42 | 43 |
    44 | 45 |
    46 | ); 47 | } 48 | 49 | } 50 | 51 | export default AboutPage; -------------------------------------------------------------------------------- /app/logui-client/common/conferenceProceedings.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | class ConferenceProceedings extends React.Component { 4 | 5 | render() { 6 | return ( 7 | 8 | @inproceedings{"{"}maxwell2021logui,
    9 |     author = {"{"}Maxwell, David and Hauff, Claudia{"}"},
    10 |     title = {"{"}LogUI: Contemporary Logging Infrastructure for Web-Based Experiments{"}"},
    11 |     booktitle = {"{"}Advanced in Information Retrieval (Proc. ECIR){"}"},
    12 |     year = {"{"}2021{"}"},
    13 |     pages = {"{"}525--530{"}"}
    14 | {"}"} 15 |
    16 | ); 17 | } 18 | 19 | } 20 | 21 | export default ConferenceProceedings; -------------------------------------------------------------------------------- /app/logui-client/common/footer.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Constants from '../constants'; 3 | import LogUIDevice from '../common/logUIDevice'; 4 | 5 | class Footer extends React.Component { 6 | 7 | render() { 8 | return ( 9 |
    10 |
    11 | Control App, version {Constants.LOGUI_CLIENTAPP_VERSION}
    12 | Running on {LOGUI_CLIENTAPP_HOSTNAME} 13 |
    14 |
    15 | Delft University of Technology 16 |
    17 |
    18 | ); 19 | } 20 | 21 | } 22 | 23 | export default Footer; -------------------------------------------------------------------------------- /app/logui-client/common/header.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {Link} from 'react-router-dom'; 3 | import Constants from '../constants'; 4 | 5 | class HeaderPageComponent extends React.Component { 6 | 7 | render() { 8 | return ( 9 |
    10 | LogUI Logo 11 | v{`${Constants.LOGUI_CLIENTAPP_VERSION}`} 12 |
    13 | ); 14 | } 15 | 16 | } 17 | 18 | export default HeaderPageComponent; -------------------------------------------------------------------------------- /app/logui-client/common/landing.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import TrailItem from '../nav/trail/trailItem'; 3 | import Footer from './footer'; 4 | import LogUIDevice from './logUIDevice'; 5 | import {Link} from 'react-router-dom'; 6 | 7 | class LandingPage extends React.Component { 8 | 9 | constructor(props) { 10 | super(props); 11 | this.getMessage = this.getMessage.bind(this); 12 | } 13 | 14 | getTrail() { 15 | return [ 16 | , 17 | ]; 18 | } 19 | 20 | componentDidMount() { 21 | this.props.clientMethods.setMenuComponent(null); 22 | this.props.clientMethods.setTrailComponent(this.getTrail()); 23 | 24 | this.props.clientMethods.clearLandingMessage(); 25 | } 26 | 27 | getMessage() { 28 | switch(this.props.landingMessage) { 29 | case 1: 30 | return You have successfully logged out of the app. 31 | case 2: 32 | return You are now logged in. 33 | default: 34 | return null; 35 | } 36 | } 37 | 38 | render() { 39 | let message = this.getMessage(); 40 | 41 | return( 42 |
    43 |
    44 |

    Welcome to !

    45 | 46 | {message ? 47 |
    48 | {message} 49 |
    50 | : ""} 51 | 52 |

    53 | is a new way of logging user interactions within your web-based experiments. This is the server app, which allows you to control what web applications that you want to track. 54 |

    55 | 56 | {this.props.isLoggedIn ? 57 |

    Select one of the options from below to begin.

    58 | : 59 |

    Before you can do that, you need to login. Click here to do so.

    60 | } 61 | 62 | {this.props.isLoggedIn ? 63 |
      64 |
    • 65 | 66 | Manage Applications 67 | Click here to manage what applications this instance of is tracking. 68 | 69 | 70 |
    • 71 |
    • 72 | 73 | Settings 74 | Tweak settings related to this instance of the server. 75 | 76 | 77 |
    • 78 |
    • 79 | 80 | About 81 | View more information about . 82 | 83 | 84 |
    • 85 |
    • 86 | 87 | Logout 88 | Click here to safely logout of this instance of the server app. 89 | 90 | 91 |
    • 92 |
    93 | : ""} 94 |
    95 | 96 |
    97 |
    98 | ) 99 | }; 100 | 101 | } 102 | 103 | export default LandingPage; -------------------------------------------------------------------------------- /app/logui-client/common/loadingSplash.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Constants from '../constants'; 3 | 4 | class LoadingSplash extends React.Component { 5 | 6 | constructor(props) { 7 | super(props); 8 | 9 | this.state = { 10 | hideElement: false 11 | }; 12 | } 13 | 14 | componentDidUpdate() { 15 | if (this.props.hideSplashScreen && !this.state.hideElement) { 16 | setTimeout( 17 | () => { 18 | this.setState({ 19 | hideElement: true 20 | }); 21 | }, 22 | Constants.SPLASH_FADE_DURATION 23 | ); 24 | } 25 | } 26 | 27 | render() { 28 | return ( 29 |
    30 | LogUI Logo 31 |
    32 | ); 33 | } 34 | 35 | } 36 | 37 | export default LoadingSplash; -------------------------------------------------------------------------------- /app/logui-client/common/logUIDevice.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | class LogUIDevice extends React.Component { 4 | 5 | render() { 6 | return( 7 | LogUI 8 | ); 9 | } 10 | 11 | } 12 | 13 | export default LogUIDevice; -------------------------------------------------------------------------------- /app/logui-client/common/notFound.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import TrailItem from '../nav/trail/trailItem'; 3 | import LogUIDevice from '../common/logUIDevice'; 4 | 5 | class NotFoundPage extends React.Component { 6 | 7 | getTrail() { 8 | return [ 9 | , 10 | , 11 | ]; 12 | } 13 | 14 | componentDidMount() { 15 | this.props.clientMethods.setMenuComponent(null); 16 | this.props.clientMethods.setTrailComponent(this.getTrail()); 17 | } 18 | 19 | render() { 20 | return ( 21 |
    22 |
    23 |

    Page Not Found

    24 |

    could not find the page you are looking for.

    25 |
    26 |
    27 | ); 28 | } 29 | 30 | } 31 | 32 | export default NotFoundPage; -------------------------------------------------------------------------------- /app/logui-client/constants.js: -------------------------------------------------------------------------------- 1 | const Constants = { 2 | LOGUI_CLIENTAPP_VERSION: '0.5.4', 3 | SPLASH_PERSIST_DELAY: 1000, // How long should the splash screen display on the screen for before it begins to fade out (after loading is complete)? 4 | SPLASH_FADE_DURATION: 700, // How long should the fade out effect last for? Ensure this matches @time-loadingSplash-fade-duration in loadingSplash.less! 5 | 6 | SESSIONSTORAGE_AUTH_TOKEN: 'logui-authToken', // The key in SessionStorage for keeping tabs of the authentication token to communicate with the server. 7 | 8 | SERVER_API_ROOT: '/api/', // The root of the API server. 9 | } 10 | 11 | export default Constants; -------------------------------------------------------------------------------- /app/logui-client/flight/add.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Menu from '../applications/menu'; 3 | import TrailItem from '../nav/trail/trailItem'; 4 | import Constants from '../constants'; 5 | import LogUIDevice from '../common/logUIDevice'; 6 | import {Redirect} from 'react-router-dom'; 7 | 8 | class FlightAddPage extends React.Component { 9 | 10 | constructor(props) { 11 | super(props); 12 | 13 | this.state = { 14 | hasFailed: false, 15 | appInfo: null, 16 | flightName: '', 17 | fqdn: '', 18 | formState: 0, 19 | alreadyExists: false, 20 | }; 21 | 22 | this.processForm = this.processForm.bind(this); 23 | } 24 | 25 | getTrail() { 26 | if (this.state.hasFailed || !this.state.appInfo) { 27 | return []; 28 | } 29 | 30 | return [ 31 | , 32 | , 33 | , 34 | , 35 | ]; 36 | } 37 | 38 | async getAppDetails() { 39 | var response = await fetch(`${Constants.SERVER_API_ROOT}application/specific/${this.props.match.params.id}/`, { 40 | method: 'GET', 41 | headers: { 42 | 'Authorization': `jwt ${this.props.clientMethods.getLoginDetails().token}` 43 | }, 44 | }); 45 | 46 | await response.json().then(data => { 47 | if (response.status == 200) { 48 | this.setState({ 49 | appInfo: data, 50 | }); 51 | 52 | return; 53 | } 54 | 55 | this.setState({ 56 | hasFailed: true, 57 | }) 58 | }); 59 | } 60 | 61 | async componentDidMount() { 62 | await this.getAppDetails(); 63 | this.props.clientMethods.setMenuComponent(Menu); 64 | this.props.clientMethods.setTrailComponent(this.getTrail()); 65 | this.flightNameField.focus(); 66 | } 67 | 68 | async componentDidUpdate(prevProps) { 69 | if (this.props.match.params.id !== prevProps.match.params.id) { 70 | await this.getAppDetails(); 71 | this.props.clientMethods.setTrailComponent(this.getTrail()); 72 | } 73 | } 74 | 75 | async checkFlightName(currentValue) { 76 | var response = await fetch(`${Constants.SERVER_API_ROOT}flight/${this.state.appInfo.id}/add/check/?flightName=${currentValue}`, { 77 | method: 'GET', 78 | headers: { 79 | 'Authorization': `jwt ${this.props.clientMethods.getLoginDetails().token}` 80 | }, 81 | }); 82 | 83 | await response.json().then(data => { 84 | this.checkFlightNameResponse(data); 85 | }); 86 | } 87 | 88 | checkFlightNameResponse(data) { 89 | if (!data.is_available) { 90 | this.setState({ 91 | formState: 3, 92 | alreadyExists: true, 93 | }); 94 | 95 | return; 96 | } 97 | 98 | this.setState({ 99 | formState: 1, 100 | alreadyExists: false, 101 | }); 102 | } 103 | 104 | handleFieldChange(fieldName, value) { 105 | if (fieldName == 'flightName') { 106 | this.checkFlightName(value); 107 | } 108 | 109 | let newState = {}; 110 | newState[fieldName] = value; 111 | this.setState(newState); 112 | } 113 | 114 | handleFieldFocusBlur(fieldName, isFocused) { 115 | let toState = 0; 116 | 117 | if ((this.state.alreadyExists && !isFocused) || 118 | (this.state.alreadyExists && isFocused && fieldName == 'flightName')) { 119 | this.setState({ 120 | formState: 3, 121 | }); 122 | 123 | return; 124 | } 125 | 126 | switch (fieldName) { 127 | case 'flightName': 128 | toState = isFocused ? 1 : 0; 129 | break; 130 | case 'fqdn': 131 | toState = isFocused ? 2 : 0; 132 | break; 133 | } 134 | 135 | this.setState({ 136 | formState: toState, 137 | }); 138 | } 139 | 140 | clearForm() { 141 | this.setState({ 142 | flightName: '', 143 | fqdn: '', 144 | formState: 0, 145 | alreadyExists: false, 146 | }); 147 | 148 | this.flightNameField.focus(); 149 | } 150 | 151 | processForm(e) { 152 | e.preventDefault(); 153 | 154 | if (this.state.alreadyExists || this.state.flightName == '' || this.state.fqdn == '') { 155 | return; 156 | } 157 | 158 | var response = fetch(`${Constants.SERVER_API_ROOT}flight/${this.state.appInfo.id}/add/`, { 159 | method: 'POST', 160 | headers: { 161 | 'Content-Type': 'application/json', 162 | 'Authorization': `jwt ${this.props.clientMethods.getLoginDetails().token}`, 163 | }, 164 | body: JSON.stringify({ 165 | flightName: this.state.flightName, 166 | fqdn: this.state.fqdn, 167 | }), 168 | }).then(e => { 169 | switch (e.status) { 170 | case 201: 171 | this.setState({ 172 | formState: 5, 173 | }); 174 | break; 175 | case 409: 176 | this.setState({ 177 | formState: 3, 178 | alreadyExists: true, 179 | }); 180 | break; 181 | default: 182 | this.setState({ 183 | formState: 4, 184 | }); 185 | break; 186 | } 187 | }); 188 | } 189 | 190 | renderRedirect() { 191 | if (this.state.formState == 5) { 192 | return (); 193 | } 194 | } 195 | 196 | render() { 197 | if (this.state.hasFailed) { 198 | return( 199 | 200 | ); 201 | } 202 | 203 | if (!this.state.appInfo) { 204 | return(null); // Could add a loading thing here. 205 | } 206 | 207 | let messageBox = null; 208 | 209 | // 0 is default 210 | // 1 is click on the flightName field 211 | // 2 is click on the fqdn field 212 | // 3 is when the name is already taken 213 | // 4 something went wrong submitting the form 214 | // 5 added successfully, redirect required 215 | switch (this.state.formState) { 216 | case 1: 217 | messageBox = 218 |
    219 | Enter a name for the new {this.state.appInfo.name} flight. The name is simply a label that makes it easy for you to identify a given flight. 220 |
    221 | break; 222 | case 2: 223 | messageBox = 224 |
    225 | Enter the host/domain where your experimental system resides. Ensure this is the full domain that will be used by participants. If this is not correct, logging will not work.
    226 | For example, if your system is at server.institute.nl/app/, enter server.institute.nl. If your application resides at a non-standard port (e.g. 8080, include this like server.institute.nl:8080). Bypass domain checks by entering bypass. 227 |
    228 | break; 229 | case 3: 230 | messageBox = 231 |
    232 | The specified flight name already exists for the {this.state.appInfo.name} application. Please try a different name. 233 |
    234 | break; 235 | case 4: 236 | messageBox = 237 |
    238 | Something went wrong with the submission of the form. Please resubmit, or try again later. 239 |
    240 | break; 241 | default: 242 | messageBox = 243 |
    244 | To create a new flight for {this.state.appInfo.name}, specify a name for it (unique to the application), and the hostname/domain where your experimental system is located. 245 |
    246 | } 247 | 248 | return ( 249 |
    250 | {this.renderRedirect()} 251 |
    252 |
    253 |

    Add New Flight

    254 |
    255 | 256 |

    257 | Here, you can add a new {this.state.appInfo.name} flight to track with . Fill out the form below, and then click Add New Flight to store it in the database. 258 |

    259 | 260 |
    261 | 262 |
    263 | 272 | 280 |
    281 | 282 | 283 |
    284 |
    285 | 286 | {messageBox} 287 | 288 |
    289 |
    290 |
    291 | ) 292 | }; 293 | 294 | } 295 | 296 | export default FlightAddPage; -------------------------------------------------------------------------------- /app/logui-client/flight/landing.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Menu from '../applications/menu'; 3 | import TrailItem from '../nav/trail/trailItem'; 4 | import Constants from '../constants'; 5 | import {Link, Redirect} from 'react-router-dom'; 6 | 7 | class ViewFlightsPage extends React.Component { 8 | 9 | constructor(props) { 10 | super(props); 11 | 12 | this.state = { 13 | hasFailed: false, 14 | appInfo: null, 15 | flightListing: [], 16 | }; 17 | } 18 | 19 | getTrail() { 20 | if (this.state.hasFailed || !this.state.appInfo) { 21 | return []; 22 | } 23 | 24 | return [ 25 | , 26 | , 27 | , 28 | ]; 29 | } 30 | 31 | async getAppDetails() { 32 | var response = await fetch(`${Constants.SERVER_API_ROOT}application/specific/${this.props.match.params.id}/`, { 33 | method: 'GET', 34 | headers: { 35 | 'Authorization': `jwt ${this.props.clientMethods.getLoginDetails().token}` 36 | }, 37 | }); 38 | 39 | await response.json().then(data => { 40 | if (response.status == 200) { 41 | this.setState({ 42 | appInfo: data, 43 | }); 44 | 45 | return; 46 | } 47 | 48 | this.setState({ 49 | hasFailed: true, 50 | }) 51 | }); 52 | } 53 | 54 | async getFlightListing() { 55 | var response = await fetch(`${Constants.SERVER_API_ROOT}flight/list/${this.state.appInfo.id}/`, { 56 | method: 'GET', 57 | headers: { 58 | 'Authorization': `jwt ${this.props.clientMethods.getLoginDetails().token}` 59 | }, 60 | }); 61 | 62 | await response.json().then(data => { 63 | this.setState({ 64 | flightListing: data, 65 | }); 66 | }); 67 | } 68 | 69 | async componentDidMount() { 70 | await this.getAppDetails(); 71 | await this.getFlightListing(); 72 | this.props.clientMethods.setMenuComponent(Menu); 73 | this.props.clientMethods.setTrailComponent(this.getTrail()); 74 | } 75 | 76 | async componentDidUpdate(prevProps) { 77 | if (this.props.match.params.id !== prevProps.match.params.id) { 78 | await this.getAppDetails(); 79 | await this.getFlightListing(); 80 | this.props.clientMethods.setTrailComponent(this.getTrail()); 81 | } 82 | } 83 | 84 | render() { 85 | let flightListing = this.state.flightListing; 86 | let authToken = this.props.clientMethods.getLoginDetails().token; 87 | 88 | if (this.state.hasFailed) { 89 | return( 90 | 91 | ); 92 | } 93 | 94 | if (!this.state.appInfo) { 95 | return(null); // Could add a loading thing here. 96 | } 97 | 98 | return ( 99 |
    100 |
    101 |
    102 |

    {this.state.appInfo.name}Flight Listing

    103 |
      104 |
    • Add New Flight
    • 105 |
    106 |
    107 | 108 |

    109 | Flights are the variants for each application. For example, if you are running an experiment with four conditions, your system may be run over four different variants in different locations. For this, you'd set up a flight for each experimental variant. 110 |

    111 | 112 | {this.state.flightListing.length == 0 ? 113 |

    The application {this.state.appInfo.name} currently has no flights registered. Add a new flight to start tracking interactions!

    114 | : 115 | 116 |
    117 |
    118 | 119 | Name/Domain 120 | Created At 121 | Session(s) 122 | 123 | 124 |
    125 | 126 | {Object.keys(flightListing).map(function(key) { 127 | return ( 128 | 137 | ); 138 | })} 139 |
    140 | 141 | } 142 |
    143 |
    144 | ); 145 | } 146 | 147 | } 148 | 149 | 150 | class FlightListItem extends React.Component { 151 | 152 | constructor(props) { 153 | super(props); 154 | 155 | this.state = { 156 | isActive: this.props.isActive, 157 | } 158 | 159 | this.toggleStatus = this.toggleStatus.bind(this); 160 | this.downloadData = this.downloadData.bind(this); 161 | }; 162 | 163 | async toggleStatus() { 164 | var response = await fetch(`${Constants.SERVER_API_ROOT}flight/info/${this.props.id}/status/`, { 165 | method: 'PATCH', 166 | headers: { 167 | 'Authorization': `jwt ${this.props.authToken}` 168 | }, 169 | }); 170 | 171 | await response.json().then(data => { 172 | this.setState({ 173 | isActive: data.is_active, 174 | }); 175 | }); 176 | }; 177 | 178 | async downloadData(event) { 179 | event.preventDefault(); 180 | 181 | var response = fetch(`${Constants.SERVER_API_ROOT}flight/download/${this.props.id}/`, { 182 | method: 'GET', 183 | headers: { 184 | 'Authorization': `jwt ${this.props.authToken}` 185 | }, 186 | }) 187 | .then(resp => resp.blob()) // Take the blob that is returned by the server 188 | .then(blob => { // "Click" the link for the blob, and it downloads. 189 | if (blob.size == 0) { 190 | alert('There is no log data available to download for this flight at present.'); 191 | return; 192 | } 193 | 194 | // To simulate a download, create a new anchor element, and add the download attribute. 195 | // Then 'click' it. This forces the browser to download the blob! 196 | let link = document.createElement('a'); 197 | link.href = URL.createObjectURL(blob); 198 | link.setAttribute('download', `logui-${this.props.id}.log`); 199 | link.click(); 200 | }); 201 | }; 202 | 203 | render() { 204 | let fqdnElement = {this.props.fqdn}; 205 | 206 | if (this.props.fqdn.toLowerCase() == 'bypass') { 207 | fqdnElement = Bypassing domain checks; 208 | } 209 | 210 | return ( 211 |
    212 | 213 | 214 | {this.props.name} 215 | {fqdnElement} 216 | 217 | 218 | {this.props.timestampSplit.time.locale} 219 | {this.props.timestampSplit.date.friendly} 220 | 221 | {this.props.sessions} 222 | Get Token 223 | this.downloadData(e)} className="icon-container icon-download dark hover">Download 224 | View Flight Sessions 225 |
    226 | ) 227 | }; 228 | 229 | } 230 | 231 | export default ViewFlightsPage; -------------------------------------------------------------------------------- /app/logui-client/flight/token.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import TrailItem from '../nav/trail/trailItem'; 3 | import Constants from '../constants'; 4 | import Menu from '../applications/menu'; 5 | import LogUIDevice from '../common/logUIDevice'; 6 | 7 | class AuthorisationTokenPage extends React.Component { 8 | 9 | constructor(props) { 10 | super(props); 11 | 12 | this.state = { 13 | flightAuthorisationToken: null, 14 | hasCopied: 0, 15 | flightInfo: null, 16 | hasFailed: false, 17 | }; 18 | 19 | this.copyAuthorisationCode = this.copyAuthorisationCode.bind(this); 20 | } 21 | 22 | async getFlightDetails() { 23 | var response = await fetch(`${Constants.SERVER_API_ROOT}flight/info/${this.props.match.params.id}/`, { 24 | method: 'GET', 25 | headers: { 26 | 'Authorization': `jwt ${this.props.clientMethods.getLoginDetails().token}` 27 | }, 28 | }); 29 | 30 | await response.json().then(data => { 31 | if (response.status == 200) { 32 | this.setState({ 33 | flightInfo: data, 34 | }); 35 | 36 | return; 37 | } 38 | 39 | this.setState({ 40 | hasFailed: true, 41 | }); 42 | }); 43 | } 44 | 45 | async getFlightAuthorisationToken() { 46 | var response = await fetch(`${Constants.SERVER_API_ROOT}flight/info/${this.props.match.params.id}/token/`, { 47 | method: 'GET', 48 | headers: { 49 | 'Authorization': `jwt ${this.props.clientMethods.getLoginDetails().token}` 50 | }, 51 | }); 52 | 53 | await response.json().then(data => { 54 | if (response.status == 200) { 55 | this.setState({ 56 | flightAuthorisationToken: data.flightAuthorisationToken, 57 | }); 58 | 59 | return; 60 | } 61 | 62 | this.setState({ 63 | hasFailed: true, 64 | }); 65 | }); 66 | } 67 | 68 | getTrail() { 69 | if (this.state.hasFailed || !this.state.flightInfo) { 70 | return []; 71 | } 72 | 73 | return [ 74 | , 75 | , 76 | , 77 | , 78 | , 79 | ]; 80 | } 81 | 82 | async componentDidMount() { 83 | await this.getFlightDetails(); 84 | await this.getFlightAuthorisationToken(); 85 | this.props.clientMethods.setMenuComponent(Menu); 86 | this.props.clientMethods.setTrailComponent(this.getTrail()); 87 | } 88 | 89 | async componentDidUpdate(prevProps) { 90 | if (this.props.match.params.id !== prevProps.match.params.id) { 91 | await this.getFlightDetails(); 92 | await this.getFlightAuthorisationToken(); 93 | this.props.clientMethods.setTrailComponent(this.getTrail()); 94 | } 95 | } 96 | 97 | copyAuthorisationCode() { 98 | let authorisationTokenElement = document.querySelector('#logui-flight-authorisation-token'); 99 | let range = document.createRange(); 100 | range.selectNode(authorisationTokenElement); 101 | 102 | window.getSelection().removeAllRanges(); 103 | window.getSelection().addRange(range); 104 | 105 | document.execCommand('copy'); 106 | 107 | window.getSelection().removeAllRanges(); 108 | 109 | this.setState({ 110 | hasCopied: true, 111 | }); 112 | 113 | setTimeout(() => { 114 | this.setState({ 115 | hasCopied: false, 116 | }); 117 | }, 2000); 118 | } 119 | 120 | render() { 121 | let flightInfo = this.state.flightInfo; 122 | 123 | if (this.state.hasFailed) { 124 | return( 125 | 126 | ); 127 | } 128 | 129 | if (!this.state.flightInfo) { 130 | return(null); // Could add a loading thing here. 131 | } 132 | 133 | return( 134 |
    135 |
    136 |
    137 |

    Flight Authorisation Token

    138 |
    139 | 140 |

    141 | In order for you to track user interactions on your application at {flightInfo.fqdn}, you need to provide the client you are using on that application with an authorisation code. This code provides the server with information telling who is connecting to it, and from where. 142 |

    143 | 144 |

    145 | The code you must use for this particular application flight is shown below. Paste this into your client configuration object. Click the button to copy it to your device's clipboard. 146 |

    147 | 148 | {this.state.flightAuthorisationToken ? 149 |
    150 | 151 | {this.state.flightAuthorisationToken} 152 | 153 |
      154 |
    • 155 |
    156 |
    157 | : 158 |

    Getting code...

    159 | } 160 | 161 |

    162 | Note that this code is unique to this particular application flight. Make sure you are using the correct code and the domain you specified is correct; any incorrect information will lead to interaction data not being logged. 163 |

    164 |
    165 |
    166 | ) 167 | }; 168 | 169 | } 170 | 171 | export default AuthorisationTokenPage; -------------------------------------------------------------------------------- /app/logui-client/nav/menu/blank.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | class BlankMenu extends React.Component { 4 | 5 | render() { 6 | return ( 7 |
    8 | ) 9 | }; 10 | 11 | } 12 | 13 | export default BlankMenu; -------------------------------------------------------------------------------- /app/logui-client/nav/menu/menu.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {Link} from 'react-router-dom'; 3 | 4 | import BlankMenu from './blank'; 5 | 6 | class MenuPageComponent extends React.Component { 7 | 8 | constructor(props) { 9 | super(props); 10 | } 11 | 12 | render() { 13 | var menuComponent = ; 14 | 15 | if (this.props.currentMenuComponent) { 16 | menuComponent = ; 17 | } 18 | 19 | return ( 20 | 34 | ) 35 | } 36 | 37 | } 38 | 39 | export default MenuPageComponent; -------------------------------------------------------------------------------- /app/logui-client/nav/trail/trail.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import TrailItem from './trailItem'; 3 | 4 | class TrailPageComponent extends React.Component { 5 | 6 | constructor(props) { 7 | super(props); 8 | } 9 | 10 | render() { 11 | var trailSequence = []; 12 | 13 | if (this.props.currentTrail) { 14 | trailSequence = this.props.currentTrail; 15 | } 16 | 17 | return ( 18 | 23 | ); 24 | } 25 | 26 | } 27 | 28 | export default TrailPageComponent; -------------------------------------------------------------------------------- /app/logui-client/nav/trail/trailItem.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {Link} from 'react-router-dom'; 3 | 4 | class TrailItem extends React.Component { 5 | 6 | constructor(props) { 7 | super(props); 8 | } 9 | 10 | render() { 11 | if (!this.props.to) { 12 | return(
  • {this.props.displayText}
  • ); 13 | } 14 | 15 | return(
  • {this.props.displayText}
  • ); 16 | } 17 | 18 | } 19 | 20 | export default TrailItem; -------------------------------------------------------------------------------- /app/logui-client/session/landing.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Menu from '../applications/menu'; 3 | import TrailItem from '../nav/trail/trailItem'; 4 | import Constants from '../constants'; 5 | import LogUIDevice from '../common/logUIDevice'; 6 | import {Link, Redirect} from 'react-router-dom'; 7 | 8 | 9 | class ViewSessionPage extends React.Component { 10 | 11 | constructor(props) { 12 | super(props); 13 | 14 | this.state = { 15 | hasFailed: false, 16 | flightInfo: null, 17 | sessionListing: [], 18 | }; 19 | 20 | this.toggleFlightStatus = this.toggleFlightStatus.bind(this); 21 | } 22 | 23 | getTrail() { 24 | if (this.state.hasFailed || !this.state.flightInfo) { 25 | return []; 26 | } 27 | 28 | return [ 29 | , 30 | , 31 | , 32 | , 33 | ]; 34 | } 35 | 36 | async getFlightDetails() { 37 | var response = await fetch(`${Constants.SERVER_API_ROOT}flight/info/${this.props.match.params.id}/`, { 38 | method: 'GET', 39 | headers: { 40 | 'Authorization': `jwt ${this.props.clientMethods.getLoginDetails().token}` 41 | }, 42 | }); 43 | 44 | await response.json().then(data => { 45 | if (response.status == 200) { 46 | this.setState({ 47 | flightInfo: data, 48 | }); 49 | 50 | return; 51 | } 52 | 53 | this.setState({ 54 | hasFailed: true, 55 | }); 56 | }); 57 | } 58 | 59 | async getSessionListings() { 60 | var response = await fetch(`${Constants.SERVER_API_ROOT}session/list/${this.props.match.params.id}/`, { 61 | method: 'GET', 62 | headers: { 63 | 'Authorization': `jwt ${this.props.clientMethods.getLoginDetails().token}` 64 | }, 65 | }); 66 | 67 | await response.json().then(data => { 68 | this.setState({ 69 | sessionListing: data, 70 | }); 71 | }); 72 | } 73 | 74 | async componentDidMount() { 75 | await this.getFlightDetails(); 76 | await this.getSessionListings(); 77 | this.props.clientMethods.setMenuComponent(Menu); 78 | this.props.clientMethods.setTrailComponent(this.getTrail()); 79 | } 80 | 81 | async componentDidUpdate(prevProps) { 82 | if (this.props.match.params.id !== prevProps.match.params.id) { 83 | await this.getFlightDetails(); 84 | await this.getSessionListings(); 85 | this.props.clientMethods.setTrailComponent(this.getTrail()); 86 | } 87 | } 88 | 89 | async toggleFlightStatus() { 90 | var response = await fetch(`${Constants.SERVER_API_ROOT}flight/info/${this.state.flightInfo.id}/status/`, { 91 | method: 'PATCH', 92 | headers: { 93 | 'Authorization': `jwt ${this.props.clientMethods.getLoginDetails().token}` 94 | }, 95 | }); 96 | 97 | await response.json().then(data => { 98 | let updatedFlightInfo = this.state.flightInfo; 99 | updatedFlightInfo.is_active = data.is_active; 100 | 101 | this.setState({ 102 | flightInfo: updatedFlightInfo, 103 | }); 104 | }); 105 | } 106 | 107 | render() { 108 | let sessionListing = this.state.sessionListing; 109 | 110 | if (this.state.hasFailed) { 111 | return( 112 | 113 | ); 114 | } 115 | 116 | if (!this.state.flightInfo) { 117 | return(null); // Could add a loading thing here. 118 | } 119 | 120 | return ( 121 |
    122 |
    123 |
    124 |

    {this.state.flightInfo.name}{this.state.flightInfo.application.name}

    125 |
      126 |
    • View Authorisation Token
    • 127 |
    128 |
    129 | 130 |

    131 | Browsing sessions that have been captured on {this.state.flightInfo.application.name} by are listed here. Metadata about each session (e.g., the browser used) is shown. 132 |

    133 | {this.state.flightInfo.is_active ? 134 |

    is currently accepting new sessions for this flight.

    135 | : 136 |

    is not currently accepting new sessions for this flight.

    137 | } 138 | 139 | {sessionListing.length == 0 ? 140 |

    has not yet recorded any sessions for this flight.

    141 | 142 | : 143 | 144 |
    145 |
    146 | IP Address 147 | Start Timestamp 148 | End Timestamp 149 | 150 | 151 | 152 |
    153 | 154 | {Object.keys(sessionListing).map(function(key) { 155 | return ( 156 | 163 | ); 164 | })} 165 |
    166 | } 167 |
    168 |
    169 | ); 170 | } 171 | 172 | } 173 | 174 | class SessionListItem extends React.Component { 175 | 176 | constructor(props) { 177 | super(props); 178 | } 179 | 180 | getOSFamilyIconClass(familyString) { 181 | familyString = familyString.toLowerCase(); 182 | let iconClass = 'unknown'; 183 | 184 | if (familyString.includes('mac') || familyString.includes('iphone')) { 185 | iconClass = 'apple'; 186 | } 187 | else if (familyString.includes('windows')) { 188 | iconClass = 'windows'; 189 | } 190 | else if (familyString.includes('linux')) { 191 | iconClass = 'linux'; 192 | } 193 | else if (familyString.includes('android')) { 194 | iconClass = 'android'; 195 | } 196 | 197 | return iconClass; 198 | } 199 | 200 | getBrowserFamilyIconClass(familyString) { 201 | familyString = familyString.toLowerCase(); 202 | let iconClass = 'browser'; 203 | 204 | if (familyString.includes('chrome') || familyString.includes('chromium')) { 205 | iconClass = 'chrome'; 206 | } 207 | else if (familyString.includes('firefox')) { 208 | iconClass = 'firefox'; 209 | } 210 | else if (familyString.includes('safari')) { 211 | iconClass = 'safari'; 212 | } 213 | else if (familyString.includes('opera')) { 214 | iconClass = 'opera' 215 | } 216 | else if (familyString.includes('edg')) { 217 | iconClass = 'edge' 218 | } 219 | 220 | return iconClass; 221 | } 222 | 223 | render() { 224 | let iconClassOS = this.getOSFamilyIconClass(this.props.agentDetails.os.family); 225 | let iconClassBrowser = this.getBrowserFamilyIconClass(this.props.agentDetails.browser.family); 226 | 227 | return ( 228 |
    229 | 230 | {this.props.ip} 231 | {this.props.id} 232 | 233 | 234 | {this.props.splitTimestamps.start_timestamp.time.locale} 235 | {this.props.splitTimestamps.start_timestamp.date.friendly} 236 | 237 | 238 | {this.props.splitTimestamps.end_timestamp ? 239 | 240 | {this.props.splitTimestamps.end_timestamp.time.locale} 241 | {this.props.splitTimestamps.end_timestamp.date.friendly} 242 | 243 | 244 | : 245 | 246 | 247 | - 248 | 249 | } 250 | 251 | 252 | 253 | 254 | 255 |
    256 | ) 257 | }; 258 | 259 | } 260 | 261 | 262 | export default ViewSessionPage; -------------------------------------------------------------------------------- /app/logui-client/settings/landing.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import TrailItem from '../nav/trail/trailItem'; 3 | import LogUIDevice from '../common/logUIDevice'; 4 | 5 | class SettingsLandingPage extends React.Component { 6 | 7 | getTrail() { 8 | return [ 9 | , 10 | , 11 | ]; 12 | } 13 | 14 | componentDidMount() { 15 | this.props.clientMethods.setMenuComponent(Menu); 16 | this.props.clientMethods.setTrailComponent(this.getTrail()); 17 | } 18 | 19 | render() { 20 | return( 21 |
    22 |
    23 |

    Settings

    24 |
    25 | 26 |

    27 | As settings are added allowing you to customise this instance of the server, they will appear here. 28 |

    29 |
    30 | ) 31 | }; 32 | 33 | } 34 | 35 | export class Menu extends React.Component { 36 | render() { 37 | return( 38 |
    39 |

    Settings

    40 | 41 | {/*

    Global Settings

    42 | 43 | */} 46 |
    47 | ) 48 | } 49 | } 50 | 51 | export default SettingsLandingPage; -------------------------------------------------------------------------------- /app/logui-client/user/landing.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Menu from './menu'; 3 | import TrailItem from '../nav/trail/trailItem'; 4 | import LogUIDevice from '../common/logUIDevice'; 5 | import {Link} from 'react-router-dom'; 6 | 7 | class UserLandingPage extends React.Component { 8 | 9 | constructor(props) { 10 | super(props); 11 | } 12 | 13 | getTrail() { 14 | return [ 15 | , 16 | , 17 | ]; 18 | } 19 | 20 | componentDidMount() { 21 | this.props.clientMethods.setMenuComponent(Menu); 22 | this.props.clientMethods.setTrailComponent(this.getTrail()); 23 | } 24 | 25 | render() { 26 | return( 27 |
    28 |
    29 |

    User Management

    30 |
    31 | 32 |

    33 | As more user management features are added to the control app, they will appear here. 34 |

    35 | 36 | {this.props.isLoggedIn ? 37 |

    You can logout from the control app by clicking here.

    38 | : 39 |

    You can login to the control app by clicking here.

    40 | } 41 |
    42 | ) 43 | }; 44 | 45 | } 46 | 47 | export default UserLandingPage; -------------------------------------------------------------------------------- /app/logui-client/user/login.js: -------------------------------------------------------------------------------- 1 | import React, { useDebugValue } from 'react'; 2 | import {Redirect} from 'react-router-dom'; 3 | import Menu from './menu'; 4 | import TrailItem from '../nav/trail/trailItem'; 5 | import Footer from '../common/footer'; 6 | import LogUIDevice from '../common/logUIDevice'; 7 | 8 | class UserLoginPage extends React.Component { 9 | constructor(props) { 10 | super(props); 11 | 12 | this.doLogin = this.doLogin.bind(this); 13 | 14 | this.state = { 15 | username: '', 16 | password: '', 17 | loginState: 0, // 0 for initial, 1 for bad username/password, 2 for successful login. 18 | }; 19 | } 20 | 21 | getTrail() { 22 | return [ 23 | , 24 | , 25 | , 26 | ]; 27 | } 28 | 29 | componentDidMount() { 30 | if (!this.props.isLoggedIn) { 31 | this.props.clientMethods.setMenuComponent(Menu); 32 | this.props.clientMethods.setTrailComponent(this.getTrail()); 33 | this.usernameField.focus(); 34 | } 35 | } 36 | 37 | componentWillUnmount() { 38 | this.setState = (state,callback)=>{ 39 | return; 40 | }; 41 | } 42 | 43 | async doLogin(e) { 44 | e.preventDefault(); 45 | var response = await this.props.clientMethods.login(this.state.username, this.state.password); 46 | 47 | if (!response.loginSuccess) { 48 | this.setState({ 49 | loginState: 1, 50 | password: '', 51 | }); 52 | 53 | this.passwordField.value = ''; 54 | return; 55 | } 56 | 57 | this.setState({ 58 | loginState: 2 59 | }); 60 | } 61 | 62 | updateUsername(value) { 63 | this.setState({ 64 | username: value, 65 | }); 66 | } 67 | 68 | updatePassword(value) { 69 | this.setState({ 70 | password: value, 71 | }); 72 | } 73 | 74 | render() { 75 | if (this.props.isLoggedIn) { 76 | return (); 77 | } 78 | else { 79 | var loginMessage = "Enter your username and password into the input fields, and click the Login button."; 80 | 81 | switch (this.state.loginState) { 82 | case 1: 83 | loginMessage = "An unrecognised username and/or password were supplied. Please try again."; 84 | break; 85 | case 2: 86 | return (); 87 | } 88 | 89 | return( 90 |
    91 |
    92 |
    93 |

    Login

    94 |
    95 | 96 |

    97 | Welcome to ! This is the interface from where you can control this instance of the LogUI server; from creating new application references, to downloading interaction logs. 98 |

    99 | 100 |
    101 | 102 |
    103 | 107 | 111 |
    112 | 113 | 114 |
    115 |
    116 | 117 |
    118 | {loginMessage} 119 |
    120 | 121 |
    122 |
    123 | 124 |
    125 | 126 |
    127 | ); 128 | } 129 | }; 130 | 131 | } 132 | 133 | export default UserLoginPage; -------------------------------------------------------------------------------- /app/logui-client/user/logout.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {Redirect} from 'react-router-dom'; 3 | 4 | class UserLogoutPage extends React.Component { 5 | constructor(props) { 6 | super(props); 7 | 8 | props.clientMethods.logout(); 9 | } 10 | 11 | render() { 12 | return (); 13 | }; 14 | 15 | } 16 | 17 | export default UserLogoutPage; -------------------------------------------------------------------------------- /app/logui-client/user/menu.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | class Menu extends React.Component { 4 | 5 | constructor(props) { 6 | super(props); 7 | } 8 | 9 | render() { 10 | return( 11 |
    12 |

    User Management

    13 | 14 |

    {this.props.isLoggedIn ? this.props.clientMethods.getLoginDetails().user.username : "Current User"}

    15 | 16 |
      17 | {!this.props.isLoggedIn ?
    • Login
    • : null} 18 | {this.props.isLoggedIn ?
    • Logout
    • : null} 19 |
    20 |
    21 | ) 22 | } 23 | } 24 | 25 | export default Menu; -------------------------------------------------------------------------------- /docker-compose.yaml: -------------------------------------------------------------------------------- 1 | # 2 | # LogUI Server docker-compose File 3 | # 4 | # Author: David Maxwell 5 | # Date: 2021-03-26 6 | # 7 | 8 | version: "3.0" 9 | 10 | services: 11 | proxy: 12 | build: 13 | context: ./proxy 14 | restart: always 15 | depends_on: 16 | - http-worker 17 | - websocket-worker 18 | ports: 19 | - ${PROXY_LISTEN_ON}:8000 20 | volumes: 21 | - static:/logui/worker/static/ 22 | networks: 23 | - frontend 24 | 25 | http-worker: 26 | build: 27 | context: ./ 28 | dockerfile: ./worker/Dockerfile 29 | args: 30 | - SECRET_KEY=${SECRET_KEY} # This is required for collectstatic to work. 31 | env_file: .env 32 | environment: 33 | - PYTHONDONTWRITEBYTECODE=1 34 | - PYTHONUNBUFFERED=1 35 | restart: always 36 | volumes: 37 | - static:/logui/worker/static/ 38 | networks: 39 | - frontend 40 | - backend 41 | 42 | websocket-worker: 43 | build: 44 | context: ./ 45 | dockerfile: ./worker/Dockerfile 46 | args: 47 | - SECRET_KEY=${SECRET_KEY} 48 | command: daphne -b 0.0.0.0 -p 8000 worker.asgi:logui_application 49 | env_file: .env 50 | environment: 51 | - PYTHONDONTWRITEBYTECODE=1 52 | - PYTHONUNBUFFERED=1 53 | restart: always 54 | volumes: 55 | - static:/logui/worker/static/ 56 | networks: 57 | - frontend 58 | - backend 59 | 60 | db: 61 | image: postgres 62 | restart: always 63 | environment: 64 | - POSTGRES_USER=postgres 65 | - POSTGRES_PASSWORD=${DATABASE_PASSWORD} 66 | - POSTGRES_DB=logui 67 | # - PGDATA=/logui/data/ 68 | volumes: 69 | - db:/var/lib/postgresql/data 70 | networks: 71 | - backend 72 | 73 | mongo: 74 | image: mongo 75 | restart: always 76 | environment: 77 | - MONGO_INITDB_ROOT_USERNAME=mongo 78 | - MONGO_INITDB_ROOT_PASSWORD=${DATABASE_PASSWORD} 79 | volumes: 80 | - mongo:/data/db 81 | - mongo:/data/configdb 82 | networks: 83 | - backend 84 | 85 | volumes: 86 | static: 87 | db: 88 | mongo: 89 | 90 | networks: 91 | frontend: 92 | backend: -------------------------------------------------------------------------------- /proxy/Dockerfile: -------------------------------------------------------------------------------- 1 | # 2 | # LogUI Server Proxy Dockerfile 3 | # 4 | # Author: David Maxwell 5 | # Date: 2020-11-16 6 | # 7 | 8 | FROM nginx:1.17-alpine 9 | 10 | LABEL maintainer="maxwelld90@acm.org" 11 | 12 | COPY ./nginx.conf /etc/nginx 13 | COPY ./responses /usr/share/nginx/html/responses 14 | 15 | CMD ["nginx", "-g", "daemon off;"] -------------------------------------------------------------------------------- /proxy/nginx.conf: -------------------------------------------------------------------------------- 1 | user root; 2 | worker_processes auto; 3 | 4 | error_log /var/log/nginx/error.log warn; 5 | 6 | events { 7 | worker_connections 1024; 8 | } 9 | 10 | http { 11 | include /etc/nginx/mime.types; 12 | default_type application/octet-stream; 13 | sendfile off; 14 | access_log off; 15 | keepalive_timeout 3000; 16 | 17 | upstream http-worker { 18 | server http-worker:8000; 19 | } 20 | 21 | upstream websocket-worker { 22 | server websocket-worker:8000; 23 | } 24 | 25 | server { 26 | listen 8000; 27 | root /usr/share/nginx/html; 28 | index index.html; 29 | server_name localhost; 30 | client_max_body_size 16m; 31 | proxy_intercept_errors on; 32 | 33 | error_page 404 /responses/404.html; 34 | error_page 500 /responses/500.html; 35 | error_page 502 /responses/502.html; 36 | 37 | location /static/ { 38 | alias /logui/worker/static/; 39 | } 40 | 41 | location / { 42 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 43 | proxy_set_header X-Forwarded-Proto $scheme; 44 | proxy_set_header Host $http_host; 45 | proxy_redirect off; 46 | 47 | if (!-f $request_filename) { 48 | proxy_pass http://http-worker; 49 | } 50 | } 51 | 52 | location /ws/ { 53 | proxy_pass http://websocket-worker; 54 | 55 | proxy_http_version 1.1; 56 | proxy_set_header Upgrade $http_upgrade; 57 | proxy_set_header Connection "upgrade"; 58 | proxy_redirect off; 59 | proxy_set_header Host $host; 60 | proxy_set_header X-Real-IP $remote_addr; 61 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 62 | proxy_set_header X-Forwarded-Host $server_name; 63 | } 64 | } 65 | } -------------------------------------------------------------------------------- /proxy/responses/404.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | LogUI 11 | 12 | 13 | 14 | LogUI Logo 15 |

    Not Found (HTTP 404)

    16 |

    17 | LogUI couldn't find the page you are looking for. 18 | Check the path you have provided, and try again. 19 |

    20 | 21 | 22 | -------------------------------------------------------------------------------- /proxy/responses/500.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | LogUI 11 | 12 | 13 | 14 | LogUI Logo 15 |

    Internal Server Error (HTTP 500)

    16 |

    17 | Something went wrong with the LogUI server. Sorry about this! 18 | If you're the administrator, check the server logs to see what has happened. 19 |

    20 | 21 | 22 | -------------------------------------------------------------------------------- /proxy/responses/502.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | LogUI 11 | 12 | 13 | 14 | LogUI Logo 15 |

    Server Starting Up (HTTP 502)

    16 |

    17 | If you've just started the LogUI docker instance, please wait 20-30 seconds for everything to fully start, and refresh this page. 18 | Otherwise, check the output log to see what's going on — at the moment, we can't communicate with the HTTP server. 19 |

    20 | 21 | 22 | -------------------------------------------------------------------------------- /requirements/development.txt: -------------------------------------------------------------------------------- 1 | asgiref==3.3.1 2 | attrs==20.3.0 3 | autobahn==20.7.1 4 | Automat==20.2.0 5 | calmjs.parse==1.2.5 6 | cffi==1.14.3 7 | channels==3.0.2 8 | constantly==15.1.0 9 | cryptography==3.2.1 10 | daphne==3.0.1 11 | Django==3.1.3 12 | django-appconf==1.0.4 13 | django-compressor==2.4 14 | django-cors-headers==3.7.0 15 | djangorestframework==3.12.2 16 | djangorestframework-jwt==1.11.0 17 | gunicorn==20.0.4 18 | hyperlink==20.0.1 19 | idna==2.10 20 | incremental==17.5.0 21 | ply==3.11 22 | pyasn1==0.4.8 23 | pyasn1-modules==0.2.8 24 | pycparser==2.20 25 | PyHamcrest==2.0.2 26 | PyJWT==1.7.1 27 | pymongo==3.11.3 28 | pyOpenSSL==19.1.0 29 | python-dateutil==2.8.1 30 | pytz==2020.4 31 | rcssmin==1.0.6 32 | rjsmin==1.1.0 33 | service-identity==18.1.0 34 | six==1.15.0 35 | sqlparse==0.4.1 36 | Twisted==20.3.0 37 | txaio==20.4.1 38 | ua-parser==0.10.0 39 | user-agents==2.2.0 40 | zope.interface==5.2.0 41 | -------------------------------------------------------------------------------- /requirements/requirements.txt: -------------------------------------------------------------------------------- 1 | asgiref==3.3.1 2 | attrs==20.3.0 3 | autobahn==20.7.1 4 | Automat==20.2.0 5 | calmjs.parse==1.2.5 6 | cffi==1.14.3 7 | channels==3.0.2 8 | constantly==15.1.0 9 | cryptography==3.2.1 10 | daphne==3.0.1 11 | Django==3.1.3 12 | django-appconf==1.0.4 13 | django-compressor==2.4 14 | django-cors-headers==3.7.0 15 | djangorestframework==3.12.2 16 | djangorestframework-jwt==1.11.0 17 | gunicorn==20.0.4 18 | hyperlink==20.0.1 19 | idna==2.10 20 | incremental==17.5.0 21 | ply==3.11 22 | psycopg2==2.8.6 23 | pyasn1==0.4.8 24 | pyasn1-modules==0.2.8 25 | pycparser==2.20 26 | PyHamcrest==2.0.2 27 | PyJWT==1.7.1 28 | pymongo==3.11.3 29 | pyOpenSSL==19.1.0 30 | python-dateutil==2.8.1 31 | pytz==2020.4 32 | rcssmin==1.0.6 33 | rjsmin==1.1.0 34 | service-identity==18.1.0 35 | six==1.15.0 36 | sqlparse==0.4.1 37 | Twisted==20.3.0 38 | txaio==20.4.1 39 | ua-parser==0.10.0 40 | user-agents==2.2.0 41 | zope.interface==5.2.0 42 | -------------------------------------------------------------------------------- /scripts/create_env.ps1: -------------------------------------------------------------------------------- 1 | <# 2 | 3 | LogUI Server First Run Script (Windows PowerShell version) 4 | Creates an environment file, .env, for the Docker instance to use. 5 | 6 | Author: David Maxwell 7 | Date: 2021-03-25 8 | 9 | #> 10 | 11 | Add-Type -AssemblyName System.Web 12 | 13 | $hostname = $env:computername 14 | $secret_key = [System.Web.Security.Membership]::GeneratePassword(50,10).replace('=','!').replace('#','!') 15 | $db_password = [System.Web.Security.Membership]::GeneratePassword(8,1).replace('=','!').replace('#','!') 16 | 17 | Copy-Item ../.env.example -Destination ../.env 18 | 19 | ((Get-Content -path ../.env -Raw) -Replace '<>', $hostname) | Set-Content -Path ../.env 20 | ((Get-Content -path ../.env -Raw) -Replace '<>', $secret_key) | Set-Content -Path ../.env 21 | ((Get-Content -path ../.env -Raw) -Replace '<>', $db_password) | Set-Content -Path ../.env 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /scripts/create_env.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # 4 | # LogUI Server First Run Script 5 | # Creates an environment file, .env, for the Docker instance to use. 6 | # 7 | # Author: David Maxwell 8 | # Date: 2021-03-19 9 | # 10 | 11 | HOSTNAME=$(uname -n) 12 | SECRET_KEY=$(LC_CTYPE=C tr -dc A-Za-z0-9_\!\@\#\$\%\^\*\(\)-+= < /dev/urandom | head -c 50 | xargs) 13 | DB_PASSWORD=$(LC_CTYPE=C tr -dc A-Za-z0-9 < /dev/urandom | head -c 8 | xargs) 14 | 15 | cp ../.env.example ../.env 16 | awk -v hostname="$HOSTNAME" -v secret_key="$SECRET_KEY" -v password="$DB_PASSWORD" '{gsub("<>", hostname, $0); gsub("<>", secret_key, $0); gsub("<>", password, $0); print}' ../.env > ../.tmp && mv -f ../.tmp ../.env -------------------------------------------------------------------------------- /scripts/create_user.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | REM 4 | REM LogUI Server User Creation Script (Windows Batch version) 5 | REM Creates a new user account for the LogUI server control app. 6 | REM Assumes that the LogUI server docker instance is running, and that the HTTP worker is at the expected container name. 7 | REM 8 | REM Author: David Maxwell 9 | REM Date: 2021-03-25 10 | REM 11 | 12 | docker exec -it logui_http-worker_1 python manage.py createsuperuser --username=%1 -------------------------------------------------------------------------------- /scripts/create_user.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # 4 | # LogUI Server User Creation Script 5 | # Creates a new user account for the LogUI server control app. 6 | # Assumes that the LogUI server docker instance is running, and that the HTTP worker is at the expected container name. 7 | # 8 | # Author: David Maxwell 9 | # Date: 2021-03-19 10 | # 11 | 12 | docker exec -it logui_http-worker_1 python manage.py createsuperuser --username=$1 -------------------------------------------------------------------------------- /scripts/dev_db.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | cd ../worker 4 | rm db.sqlite3 5 | python manage.py makemigrations --settings=worker.settings.development 6 | python manage.py migrate --settings=worker.settings.development 7 | python manage.py createsuperuser --settings=worker.settings.development -------------------------------------------------------------------------------- /scripts/dev_server.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | cd ../worker 4 | rm -rf collectedstatic 5 | python manage.py collectstatic --noinput --settings=worker.settings.development 6 | python manage.py compress --settings=worker.settings.development 7 | python manage.py runserver --settings=worker.settings.development -------------------------------------------------------------------------------- /static/logui/css/error.css: -------------------------------------------------------------------------------- 1 | html { 2 | background-color: #46509E; 3 | } 4 | 5 | body { 6 | margin: 0 auto 0 auto; 7 | width: 60%; 8 | font-family: Arial, Helvetica, sans-serif; 9 | background-color: #46509E; 10 | color: #FFFFFF; 11 | } 12 | 13 | div#approot { 14 | display: block; 15 | } 16 | 17 | img { 18 | display: block; 19 | width: 30%; 20 | height: auto; 21 | padding: 35% 0 20px; 22 | } 23 | 24 | h1 { 25 | padding: 0; 26 | margin: 0 0 15px 0; 27 | font-size: 32px; 28 | } 29 | 30 | p { 31 | margin: 0; 32 | padding: 0; 33 | font-size: 18px; 34 | line-height: 25px; 35 | } -------------------------------------------------------------------------------- /static/logui/css/normalize.css: -------------------------------------------------------------------------------- 1 | /*! normalize.css v8.0.1 | MIT License | github.com/necolas/normalize.css */ 2 | 3 | /* Document 4 | ========================================================================== */ 5 | 6 | /** 7 | * 1. Correct the line height in all browsers. 8 | * 2. Prevent adjustments of font size after orientation changes in iOS. 9 | */ 10 | 11 | html { 12 | line-height: 1.15; /* 1 */ 13 | -webkit-text-size-adjust: 100%; /* 2 */ 14 | } 15 | 16 | /* Sections 17 | ========================================================================== */ 18 | 19 | /** 20 | * Remove the margin in all browsers. 21 | */ 22 | 23 | body { 24 | margin: 0; 25 | } 26 | 27 | /** 28 | * Render the `main` element consistently in IE. 29 | */ 30 | 31 | main { 32 | display: block; 33 | } 34 | 35 | /** 36 | * Correct the font size and margin on `h1` elements within `section` and 37 | * `article` contexts in Chrome, Firefox, and Safari. 38 | */ 39 | 40 | h1 { 41 | font-size: 2em; 42 | margin: 0.67em 0; 43 | } 44 | 45 | /* Grouping content 46 | ========================================================================== */ 47 | 48 | /** 49 | * 1. Add the correct box sizing in Firefox. 50 | * 2. Show the overflow in Edge and IE. 51 | */ 52 | 53 | hr { 54 | box-sizing: content-box; /* 1 */ 55 | height: 0; /* 1 */ 56 | overflow: visible; /* 2 */ 57 | } 58 | 59 | /** 60 | * 1. Correct the inheritance and scaling of font size in all browsers. 61 | * 2. Correct the odd `em` font sizing in all browsers. 62 | */ 63 | 64 | pre { 65 | font-family: monospace, monospace; /* 1 */ 66 | font-size: 1em; /* 2 */ 67 | } 68 | 69 | /* Text-level semantics 70 | ========================================================================== */ 71 | 72 | /** 73 | * Remove the gray background on active links in IE 10. 74 | */ 75 | 76 | a { 77 | background-color: transparent; 78 | } 79 | 80 | /** 81 | * 1. Remove the bottom border in Chrome 57- 82 | * 2. Add the correct text decoration in Chrome, Edge, IE, Opera, and Safari. 83 | */ 84 | 85 | abbr[title] { 86 | border-bottom: none; /* 1 */ 87 | text-decoration: underline; /* 2 */ 88 | text-decoration: underline dotted; /* 2 */ 89 | } 90 | 91 | /** 92 | * Add the correct font weight in Chrome, Edge, and Safari. 93 | */ 94 | 95 | b, 96 | strong { 97 | font-weight: bolder; 98 | } 99 | 100 | /** 101 | * 1. Correct the inheritance and scaling of font size in all browsers. 102 | * 2. Correct the odd `em` font sizing in all browsers. 103 | */ 104 | 105 | code, 106 | kbd, 107 | samp { 108 | font-family: monospace, monospace; /* 1 */ 109 | font-size: 1em; /* 2 */ 110 | } 111 | 112 | /** 113 | * Add the correct font size in all browsers. 114 | */ 115 | 116 | small { 117 | font-size: 80%; 118 | } 119 | 120 | /** 121 | * Prevent `sub` and `sup` elements from affecting the line height in 122 | * all browsers. 123 | */ 124 | 125 | sub, 126 | sup { 127 | font-size: 75%; 128 | line-height: 0; 129 | position: relative; 130 | vertical-align: baseline; 131 | } 132 | 133 | sub { 134 | bottom: -0.25em; 135 | } 136 | 137 | sup { 138 | top: -0.5em; 139 | } 140 | 141 | /* Embedded content 142 | ========================================================================== */ 143 | 144 | /** 145 | * Remove the border on images inside links in IE 10. 146 | */ 147 | 148 | img { 149 | border-style: none; 150 | } 151 | 152 | /* Forms 153 | ========================================================================== */ 154 | 155 | /** 156 | * 1. Change the font styles in all browsers. 157 | * 2. Remove the margin in Firefox and Safari. 158 | */ 159 | 160 | button, 161 | input, 162 | optgroup, 163 | select, 164 | textarea { 165 | font-family: inherit; /* 1 */ 166 | font-size: 100%; /* 1 */ 167 | line-height: 1.15; /* 1 */ 168 | margin: 0; /* 2 */ 169 | } 170 | 171 | /** 172 | * Show the overflow in IE. 173 | * 1. Show the overflow in Edge. 174 | */ 175 | 176 | button, 177 | input { /* 1 */ 178 | overflow: visible; 179 | } 180 | 181 | /** 182 | * Remove the inheritance of text transform in Edge, Firefox, and IE. 183 | * 1. Remove the inheritance of text transform in Firefox. 184 | */ 185 | 186 | button, 187 | select { /* 1 */ 188 | text-transform: none; 189 | } 190 | 191 | /** 192 | * Correct the inability to style clickable types in iOS and Safari. 193 | */ 194 | 195 | button, 196 | [type="button"], 197 | [type="reset"], 198 | [type="submit"] { 199 | -webkit-appearance: button; 200 | } 201 | 202 | /** 203 | * Remove the inner border and padding in Firefox. 204 | */ 205 | 206 | button::-moz-focus-inner, 207 | [type="button"]::-moz-focus-inner, 208 | [type="reset"]::-moz-focus-inner, 209 | [type="submit"]::-moz-focus-inner { 210 | border-style: none; 211 | padding: 0; 212 | } 213 | 214 | /** 215 | * Restore the focus styles unset by the previous rule. 216 | */ 217 | 218 | button:-moz-focusring, 219 | [type="button"]:-moz-focusring, 220 | [type="reset"]:-moz-focusring, 221 | [type="submit"]:-moz-focusring { 222 | outline: 1px dotted ButtonText; 223 | } 224 | 225 | /** 226 | * Correct the padding in Firefox. 227 | */ 228 | 229 | fieldset { 230 | padding: 0.35em 0.75em 0.625em; 231 | } 232 | 233 | /** 234 | * 1. Correct the text wrapping in Edge and IE. 235 | * 2. Correct the color inheritance from `fieldset` elements in IE. 236 | * 3. Remove the padding so developers are not caught out when they zero out 237 | * `fieldset` elements in all browsers. 238 | */ 239 | 240 | legend { 241 | box-sizing: border-box; /* 1 */ 242 | color: inherit; /* 2 */ 243 | display: table; /* 1 */ 244 | max-width: 100%; /* 1 */ 245 | padding: 0; /* 3 */ 246 | white-space: normal; /* 1 */ 247 | } 248 | 249 | /** 250 | * Add the correct vertical alignment in Chrome, Firefox, and Opera. 251 | */ 252 | 253 | progress { 254 | vertical-align: baseline; 255 | } 256 | 257 | /** 258 | * Remove the default vertical scrollbar in IE 10+. 259 | */ 260 | 261 | textarea { 262 | overflow: auto; 263 | } 264 | 265 | /** 266 | * 1. Add the correct box sizing in IE 10. 267 | * 2. Remove the padding in IE 10. 268 | */ 269 | 270 | [type="checkbox"], 271 | [type="radio"] { 272 | box-sizing: border-box; /* 1 */ 273 | padding: 0; /* 2 */ 274 | } 275 | 276 | /** 277 | * Correct the cursor style of increment and decrement buttons in Chrome. 278 | */ 279 | 280 | [type="number"]::-webkit-inner-spin-button, 281 | [type="number"]::-webkit-outer-spin-button { 282 | height: auto; 283 | } 284 | 285 | /** 286 | * 1. Correct the odd appearance in Chrome and Safari. 287 | * 2. Correct the outline style in Safari. 288 | */ 289 | 290 | [type="search"] { 291 | -webkit-appearance: textfield; /* 1 */ 292 | outline-offset: -2px; /* 2 */ 293 | } 294 | 295 | /** 296 | * Remove the inner padding in Chrome and Safari on macOS. 297 | */ 298 | 299 | [type="search"]::-webkit-search-decoration { 300 | -webkit-appearance: none; 301 | } 302 | 303 | /** 304 | * 1. Correct the inability to style clickable types in iOS and Safari. 305 | * 2. Change font properties to `inherit` in Safari. 306 | */ 307 | 308 | ::-webkit-file-upload-button { 309 | -webkit-appearance: button; /* 1 */ 310 | font: inherit; /* 2 */ 311 | } 312 | 313 | /* Interactive 314 | ========================================================================== */ 315 | 316 | /* 317 | * Add the correct display in Edge, IE 10+, and Firefox. 318 | */ 319 | 320 | details { 321 | display: block; 322 | } 323 | 324 | /* 325 | * Add the correct display in all browsers. 326 | */ 327 | 328 | summary { 329 | display: list-item; 330 | } 331 | 332 | /* Misc 333 | ========================================================================== */ 334 | 335 | /** 336 | * Add the correct display in IE 10+. 337 | */ 338 | 339 | template { 340 | display: none; 341 | } 342 | 343 | /** 344 | * Add the correct display in IE 10. 345 | */ 346 | 347 | [hidden] { 348 | display: none; 349 | } -------------------------------------------------------------------------------- /static/logui/fonts/roboto-300-l.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/logui-framework/server/b6635943865a71f8b9e2dcc2aaafabe5bbd7ff83/static/logui/fonts/roboto-300-l.woff2 -------------------------------------------------------------------------------- /static/logui/fonts/roboto-300-le.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/logui-framework/server/b6635943865a71f8b9e2dcc2aaafabe5bbd7ff83/static/logui/fonts/roboto-300-le.woff2 -------------------------------------------------------------------------------- /static/logui/fonts/roboto-400-l.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/logui-framework/server/b6635943865a71f8b9e2dcc2aaafabe5bbd7ff83/static/logui/fonts/roboto-400-l.woff2 -------------------------------------------------------------------------------- /static/logui/fonts/roboto-400-le.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/logui-framework/server/b6635943865a71f8b9e2dcc2aaafabe5bbd7ff83/static/logui/fonts/roboto-400-le.woff2 -------------------------------------------------------------------------------- /static/logui/fonts/roboto-500-l.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/logui-framework/server/b6635943865a71f8b9e2dcc2aaafabe5bbd7ff83/static/logui/fonts/roboto-500-l.woff2 -------------------------------------------------------------------------------- /static/logui/fonts/roboto-500-le.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/logui-framework/server/b6635943865a71f8b9e2dcc2aaafabe5bbd7ff83/static/logui/fonts/roboto-500-le.woff2 -------------------------------------------------------------------------------- /static/logui/fonts/roboto-700-l.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/logui-framework/server/b6635943865a71f8b9e2dcc2aaafabe5bbd7ff83/static/logui/fonts/roboto-700-l.woff2 -------------------------------------------------------------------------------- /static/logui/fonts/roboto-700-le.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/logui-framework/server/b6635943865a71f8b9e2dcc2aaafabe5bbd7ff83/static/logui/fonts/roboto-700-le.woff2 -------------------------------------------------------------------------------- /static/logui/img/favicon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /static/logui/img/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 7 | 8 | 9 | 10 | 15 | 21 | 22 | 23 | 24 | 25 | 26 | 30 | 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /static/logui/img/tudelft-logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 9 | 10 | 13 | 17 | 19 | 21 | 30 | 34 | 38 | 41 | 43 | 46 | 47 | 49 | 55 | 57 | 59 | 61 | 62 | 63 | -------------------------------------------------------------------------------- /static/logui/less/about.less: -------------------------------------------------------------------------------- 1 | @import 'global.less'; -------------------------------------------------------------------------------- /static/logui/less/add.less: -------------------------------------------------------------------------------- 1 | @import 'global.less'; 2 | 3 | main section div.split.add form { 4 | flex: 50%; 5 | } 6 | 7 | main section div.split.add div.right { 8 | flex: 50%; 9 | } -------------------------------------------------------------------------------- /static/logui/less/applications.less: -------------------------------------------------------------------------------- 1 | @import 'global.less'; 2 | 3 | main section div.table.applications div.row { 4 | grid-template-columns: 3fr 2fr 1fr; 5 | } 6 | 7 | main section div.table div.row.double-height span span.indicator { 8 | position: relative; 9 | top: 14px; 10 | } 11 | 12 | main section div.table div.row span span.indicator { 13 | position: relative; 14 | top: 4px; 15 | } 16 | 17 | main section div.table div.row span.flights { 18 | font-size: 18pt; 19 | font-weight: 500; 20 | } -------------------------------------------------------------------------------- /static/logui/less/core.less: -------------------------------------------------------------------------------- 1 | @import 'global.less'; 2 | 3 | @colour-message-box-background: tint(@colour-logui-purple, 70%); 4 | @colour-message-box-warning: tint(@colour-red, 70%); 5 | @colour-message-box-success: tint(@colour-green, 70%); 6 | 7 | html, body { 8 | height: 100%; 9 | } 10 | 11 | body { 12 | font-family: 'Roboto', sans-serif; 13 | background-color: @colour-page-background; 14 | color: @colour-text; 15 | } 16 | 17 | a:hover { 18 | -o-transition: @time-hyperlink-hover-duration; 19 | -ms-transition: @time-hyperlink-hover-duration; 20 | -moz-transition: @time-hyperlink-hover-duration; 21 | -webkit-transition: @time-hyperlink-hover-duration; 22 | transition: @time-hyperlink-hover-duration; 23 | } 24 | 25 | .noanimation:hover { 26 | -o-transition: all 0s ease 0s; 27 | -ms-transition: all 0s ease 0s; 28 | -moz-transition: all 0s ease 0s; 29 | -webkit-transition: all 0s ease 0s; 30 | transition: all 0s ease 0s; 31 | } 32 | 33 | div#approot { 34 | display: grid; 35 | height: 100%; 36 | grid-template-columns: 350px auto; 37 | grid-template-rows: @height-row-top auto; 38 | grid-template-areas: "banner trail" 39 | "menu main"; 40 | } 41 | 42 | button { 43 | .button 44 | } 45 | 46 | button:hover { 47 | .button.hover 48 | } 49 | 50 | button[disabled] { 51 | background-color: @colour-button-disabled; 52 | cursor: default; 53 | } 54 | 55 | .button { 56 | display: inline-block; 57 | min-width: 50px; 58 | padding: 8px; 59 | font-size: 16px; 60 | line-height: 19px; 61 | border: 0; 62 | color: @colour-button-text; 63 | background-color: @colour-button; 64 | border-radius: @radius; 65 | text-decoration: none; 66 | font-weight: 500; 67 | cursor: pointer; 68 | } 69 | 70 | .button.hover { 71 | -o-transition: all 0s ease 0s; 72 | -ms-transition: all 0s ease 0s; 73 | -moz-transition: all 0s ease 0s; 74 | -webkit-transition: all 0s ease 0s; 75 | transition: all 0s ease 0s; 76 | background-color: @colour-button-hover; 77 | } 78 | 79 | .button:hover { 80 | .button.hover 81 | } 82 | 83 | .buttonInlineSeparator * { 84 | margin-right: 10px; 85 | } 86 | 87 | .buttonInlineSeparator:last-child { 88 | margin-right: 0; 89 | } 90 | 91 | span.indicator { 92 | display: block; 93 | height: 26px !important; 94 | width: 26px !important; 95 | line-height: 0 !important; 96 | padding: 0 !important; 97 | background-color: @colour-logui-purple; 98 | border-radius: 13px; 99 | } 100 | 101 | span.indicator.clickable { 102 | cursor: pointer; 103 | } 104 | 105 | span.indicator.green { 106 | background-color: @colour-green; 107 | } 108 | 109 | span.indicator.red { 110 | background-color: @colour-red; 111 | } 112 | 113 | span.indicator.orange { 114 | background-color: @colour-orange; 115 | } 116 | 117 | span.logui strong { 118 | font-weight: 500; 119 | color: @colour-logui-purple; 120 | } 121 | 122 | .message-box { 123 | position: relative; 124 | -moz-box-sizing: border-box; 125 | -webkit-box-sizing: border-box; 126 | box-sizing: border-box; 127 | padding: 10px; 128 | border-radius: @radius; 129 | font-weight: 500; 130 | overflow-wrap: break-word; 131 | } 132 | 133 | .message-box.info { 134 | background-color: @colour-message-box-background; 135 | } 136 | 137 | .message-box.warning { 138 | background-color: @colour-message-box-warning; 139 | } 140 | 141 | .message-box.success { 142 | background-color: @colour-message-box-success; 143 | } 144 | 145 | .message-box.buttons { 146 | margin-bottom: 35px; 147 | padding-bottom: 30px; 148 | } 149 | 150 | .message-box.buttons > ul.buttons { 151 | position: absolute; 152 | display: block; 153 | margin: 0; 154 | padding: 0; 155 | bottom: -15px; 156 | right: 10px; 157 | list-style-type: none; 158 | } 159 | 160 | .message-box.buttons > ul.buttons * { 161 | display: inline-block; 162 | margin-right: 15px; 163 | } 164 | 165 | .message-box.buttons > ul.buttons *:last-child { 166 | margin-right: 0; 167 | } 168 | 169 | code.block { 170 | display: block; 171 | -moz-box-sizing: border-box; 172 | -webkit-box-sizing: border-box; 173 | box-sizing: border-box; 174 | padding: 10px; 175 | border-radius: @radius; 176 | background-color: @colour-message-box-background; 177 | } 178 | 179 | .alert-text { 180 | color: @colour-red; 181 | font-weight: 400; 182 | } 183 | 184 | a { 185 | color: @colour-logui-purple; 186 | } 187 | 188 | a:hover { 189 | color: tint(@colour-logui-purple, 40%); 190 | } -------------------------------------------------------------------------------- /static/logui/less/flight.less: -------------------------------------------------------------------------------- 1 | @import 'global.less'; 2 | 3 | main section div.table.flights div.row { 4 | grid-template-columns: 40px 3fr 2fr 1fr 40px 40px; 5 | } 6 | 7 | main section div.table div.row.double-height span span.indicator { 8 | position: relative; 9 | top: 14px; 10 | } 11 | 12 | main section div.table div.row span span.indicator { 13 | position: relative; 14 | top: 4px; 15 | } 16 | 17 | main section div.table div.row span.sessions { 18 | font-size: 18pt; 19 | font-weight: 500; 20 | } -------------------------------------------------------------------------------- /static/logui/less/footer.less: -------------------------------------------------------------------------------- 1 | @import 'global.less'; 2 | 3 | main footer { 4 | flex-shrink: 0; 5 | margin-right: 20px; 6 | display: flex; 7 | align-items: center; 8 | padding: 20px 0 10px 0; 9 | background: #FFFFFF; 10 | background: linear-gradient(0deg, rgba(255,255,255,1) 92%, rgba(255,255,255,0) 100%); 11 | } 12 | 13 | main footer div { 14 | flex: 0.5; 15 | line-height: 18pt; 16 | color: tint(@colour-logui-purple, 60%); 17 | } 18 | 19 | main footer > div:last-child { 20 | text-align: right; 21 | } 22 | 23 | main footer > div:last-child a { 24 | display: inline-block; 25 | width: 160px; 26 | } 27 | 28 | main footer > div:last-child a img { 29 | width: 100%; 30 | height: auto; 31 | } -------------------------------------------------------------------------------- /static/logui/less/global.less: -------------------------------------------------------------------------------- 1 | @time-hyperlink-hover-duration: 0.2s; 2 | 3 | @radius: 6px; 4 | 5 | @colour-page-background: #FFF; 6 | @colour-text: #000; 7 | 8 | @colour-logui-purple: #46509E; 9 | @colour-green: #4A9B46; 10 | @colour-red: #AD3A3A; 11 | @colour-orange: #E3A01C; 12 | 13 | @colour-nojs-background: @colour-logui-purple; 14 | @colour-nojs-text: #FFF; 15 | 16 | @colour-button: @colour-logui-purple; 17 | @colour-button-disabled: tint(@colour-logui-purple, 80%); 18 | @colour-button-hover: tint(@colour-logui-purple, 30%); 19 | @colour-button-text: #FFF; 20 | 21 | @height-row-top: 64px; 22 | @height-menu-category-width: 50px; 23 | 24 | @width-menu-gradient: 40px; 25 | 26 | @colour-banner-background: @colour-logui-purple; 27 | @colour-banner-text: #FFF; 28 | 29 | @colour-trail-background: @colour-page-background; 30 | @colour-trail-link: @colour-logui-purple; 31 | @colour-trail-link-hover: tint(@colour-logui-purple, 60%); 32 | 33 | @height-menu-margin-top: 20px; 34 | 35 | @colour-menu-main-background: tint(@colour-logui-purple, 70%); 36 | @colour-menu-sub-background: tint(@colour-logui-purple, 80%); 37 | 38 | @colour-menu-sub-h4-colour: tint(@colour-logui-purple, 20%); 39 | @colour-menu-li-a-hover: #000; 40 | @colour-menu-li-a-hover: @colour-logui-purple; 41 | @colour-menu-li-a-hover-text: #FFF; 42 | 43 | .mono { 44 | font-family: monospace; 45 | } -------------------------------------------------------------------------------- /static/logui/less/header.less: -------------------------------------------------------------------------------- 1 | @import 'global.less'; 2 | 3 | header { 4 | grid-area: banner; 5 | position: relative; 6 | background-color: @colour-banner-background; 7 | background-image: linear-gradient(to right, @colour-banner-background, darken(@colour-banner-background, 20%)); 8 | background-size: @width-menu-gradient 100%; 9 | background-position: right; 10 | background-repeat: no-repeat; 11 | color: @colour-banner-text; 12 | } 13 | 14 | header a { 15 | position: relative; 16 | top: 4px; 17 | height: 48px; 18 | display: inline-block; 19 | } 20 | 21 | header a img { 22 | margin: 5px 0 0 10px; 23 | height: 100%; 24 | width: auto; 25 | } 26 | 27 | header span { 28 | position: absolute; 29 | top: 35px; 30 | left: 117px; 31 | font-weight: 400; 32 | font-size: 14px; 33 | } -------------------------------------------------------------------------------- /static/logui/less/loadingSplash.less: -------------------------------------------------------------------------------- 1 | @import 'global.less'; 2 | 3 | @time-loadingSplash-fade-duration: 0.7s; 4 | 5 | div#loadingSplash { 6 | width: 100%; 7 | height: 100%; 8 | position: absolute; 9 | top: 0; 10 | left: 0; 11 | z-index: 1000; 12 | background-color: @colour-nojs-background; 13 | color: @colour-nojs-text; 14 | 15 | -webkit-animation-duration: @time-loadingSplash-fade-duration; 16 | animation-duration: @time-loadingSplash-fade-duration; 17 | -webkit-animation-fill-mode: both; 18 | animation-fill-mode: both; 19 | } 20 | 21 | div#loadingSplash.fadeOut { 22 | -webkit-animation-name: fadeOutEffect; 23 | animation-name: fadeOutEffect; 24 | } 25 | 26 | @-webkit-keyframes fadeOutEffect { 27 | 0% {opacity: 1;} 28 | 100% {opacity: 0;} 29 | } 30 | 31 | @keyframes fadeOutEffect { 32 | 0% {opacity: 1;} 33 | 100% {opacity: 0;} 34 | } 35 | 36 | div#loadingSplash img { 37 | display: block; 38 | width: 25%; 39 | height: auto; 40 | margin: 200px auto 0 auto; 41 | } 42 | 43 | div#loadingSplash.hide { 44 | display: none; 45 | } -------------------------------------------------------------------------------- /static/logui/less/login.less: -------------------------------------------------------------------------------- 1 | @import 'global.less'; 2 | 3 | main section div.split.login form { 4 | flex: 50%; 5 | } 6 | 7 | main section div.split.login div.right { 8 | flex: 50%; 9 | } -------------------------------------------------------------------------------- /static/logui/less/main.less: -------------------------------------------------------------------------------- 1 | @import 'global.less'; 2 | 3 | @colour-table-border: tint(@colour-logui-purple, 50%); 4 | @colour-table-border-lighter: tint(@colour-logui-purple, 70%); 5 | @colour-table-background-hover: tint(@colour-logui-purple, 80%); 6 | @colour-table-subtitle: tint(@colour-logui-purple, 30%); 7 | 8 | main { 9 | grid-area: main; 10 | overflow-y: scroll; 11 | margin: 0 0 0 20px; 12 | position: relative; 13 | display: flex; 14 | flex-direction: column; 15 | align-items: stretch; 16 | flex-shrink: 0; 17 | } 18 | 19 | main section { 20 | flex-grow: 1; 21 | flex-shrink: 0; 22 | } 23 | 24 | main section div.header-container { 25 | margin: 20px 20px 20px 0; 26 | } 27 | 28 | main section div.header-container h1 { 29 | display: inline-block; 30 | margin: 0; 31 | padding: 0; 32 | font-size: 32px; 33 | } 34 | 35 | main section div.header-container h1 span.indicator { 36 | display: inline-block !important; 37 | margin-right: 8px; 38 | } 39 | 40 | main section div.header-container h1 span.subtitle { 41 | display: inline-block; 42 | margin-left: 10px; 43 | font-size: 16px; 44 | font-weight: 300; 45 | } 46 | 47 | main section div.header-container ul.buttons-top { 48 | float: right; 49 | height: 35px; 50 | margin: 0; 51 | padding: 0; 52 | } 53 | 54 | main section div.header-container ul.buttons-top li { 55 | display: inline-block; 56 | height: 100%; 57 | margin-right: 10px; 58 | } 59 | 60 | main section div.header-container ul.buttons-top li:last-child { 61 | margin-right: 0; 62 | } 63 | 64 | main section h2 { 65 | display: inline-block; 66 | margin: 0; 67 | padding: 0; 68 | font-size: 24px; 69 | margin-top: 5px; 70 | } 71 | 72 | main section > * { 73 | margin: 20px 20px 20px 0; 74 | } 75 | 76 | main section > p { 77 | font-size: 12pt; 78 | line-height: 20pt; 79 | } 80 | 81 | form label { 82 | display: inline-block; 83 | margin-bottom: 10px; 84 | line-height: 22pt; 85 | } 86 | 87 | form label span { 88 | display: inline-block; 89 | width: 110px; 90 | font-weight: 500; 91 | } 92 | 93 | form label input[type="text"], form label input[type="url"], form label input[type="password"] { 94 | float: right; 95 | width: 200px; 96 | padding: 4px; 97 | font-size: 13pt; 98 | } 99 | 100 | form label input[type="url"] { 101 | .mono 102 | } 103 | 104 | main section div.table { 105 | border-top: 1px solid @colour-table-border; 106 | } 107 | 108 | main section div.table strong { 109 | font-weight: 500; 110 | } 111 | 112 | main section div.table div.row { 113 | display: grid; 114 | grid-template-columns: repeat(4, 1fr); 115 | position: relative; 116 | height: 35px; 117 | line-height: 35px; 118 | border-bottom: 1px solid @colour-table-border-lighter; 119 | } 120 | 121 | main section div.table div.row:hover { 122 | background-color: @colour-table-background-hover; 123 | } 124 | 125 | main section div.table div.row.header:hover { 126 | background-color: transparent; 127 | } 128 | 129 | main section div.table div.row.double-height { 130 | height: 55px; 131 | line-height: 55px; 132 | } 133 | 134 | main section div.table div.row.header { 135 | font-size: 13pt; 136 | font-weight: 500; 137 | border-bottom: 1px solid @colour-table-border; 138 | } 139 | 140 | main section div.table div.row a.row-link { 141 | position: absolute; 142 | top: 0; 143 | left: 0; 144 | right: 0; 145 | bottom: 0; 146 | line-height: 0; 147 | font-size: 0; 148 | color: transparent; 149 | } 150 | 151 | main section div.table div.row a:not(.row-link) { 152 | position: relative; 153 | z-index: 1; 154 | } 155 | 156 | main section div.table div.row > span.centre { 157 | text-align: center; 158 | } 159 | 160 | main section div.table div.row > span.right { 161 | text-align: right; 162 | } 163 | 164 | main section div.table div.row > span:first-child { 165 | -moz-box-sizing: border-box; 166 | -webkit-box-sizing: border-box; 167 | box-sizing: border-box; 168 | padding-left: 5px; 169 | } 170 | 171 | main section div.table div.row > span:last-child { 172 | -moz-box-sizing: border-box; 173 | -webkit-box-sizing: border-box; 174 | box-sizing: border-box; 175 | padding-right: 5px; 176 | } 177 | 178 | main section div.table div.row span.double span.title { 179 | display: block; 180 | position: relative; 181 | bottom: 8px; 182 | font-size: 13pt; 183 | height: 13pt; 184 | line-height: auto; 185 | } 186 | 187 | main section div.table div.row span.double span.subtitle { 188 | display: block; 189 | position: relative; 190 | bottom: 6px; 191 | font-size: 10pt; 192 | height: 10pt; 193 | line-height: auto; 194 | color: @colour-table-subtitle; 195 | } 196 | 197 | main section div.table div.row span.icon { 198 | text-align: center; 199 | display: flex; 200 | align-items: center; 201 | justify-content: center; 202 | } 203 | 204 | main section div.table div.row span.indicator-container a { 205 | display: inline-block; 206 | width: 100%; 207 | height: 100%; 208 | } 209 | 210 | main section div.table div.row span.icon a, 211 | main section div.table div.row span.icon span { 212 | display: block; 213 | width: 28px; 214 | height: 28px; 215 | line-height: 0; 216 | font-size: 0; 217 | color: transparent; 218 | } 219 | 220 | main section div.split { 221 | display: flex; 222 | } 223 | 224 | main section div.split div.right { 225 | line-height: 18pt; 226 | } -------------------------------------------------------------------------------- /static/logui/less/nav/grid.less: -------------------------------------------------------------------------------- 1 | @import '../global.less'; 2 | 3 | @colour-menu-grid-text: @colour-logui-purple; 4 | @colour-menu-grid-text-hover: #FFFFFF; 5 | @colour-menu-grid-background: tint(@colour-logui-purple, 70%); 6 | @colour-menu-grid-background-hover: tint(@colour-logui-purple, 40%); 7 | 8 | ul.menu-grid { 9 | display: grid; 10 | grid-template-columns: 1fr 1fr; 11 | grid-auto-rows: 200px; 12 | column-gap: 20px; 13 | row-gap: 20px; 14 | list-style-type: none; 15 | padding: 0; 16 | } 17 | 18 | ul.menu-grid li a { 19 | display: block; 20 | position: relative; 21 | width: 100%; 22 | height: 100%; 23 | padding: 20px; 24 | border-radius: @radius; 25 | background-color: @colour-menu-grid-background; 26 | -moz-box-sizing: border-box; 27 | -webkit-box-sizing: border-box; 28 | box-sizing: border-box; 29 | text-decoration: none; 30 | color: @colour-menu-grid-text; 31 | } 32 | 33 | ul.menu-grid li a:hover { 34 | background-color: @colour-menu-grid-background-hover; 35 | color: @colour-menu-grid-text-hover; 36 | } 37 | 38 | ul.menu-grid li a span.header { 39 | display: block; 40 | position: relative; 41 | z-index: 1; 42 | font-size: 26px; 43 | font-weight: 500; 44 | } 45 | 46 | ul.menu-grid li a span.blurb { 47 | display: block; 48 | position: relative; 49 | z-index: 1; 50 | line-height: 18px; 51 | margin-top: 15px; 52 | } 53 | 54 | ul.menu-grid li a span.icon { 55 | position: absolute; 56 | z-index: 0; 57 | bottom: 10px; 58 | right: 10px; 59 | width: 160px; 60 | height: 160px; 61 | border-bottom-right-radius: @radius; 62 | opacity: 0.3; 63 | } -------------------------------------------------------------------------------- /static/logui/less/nav/menu.less: -------------------------------------------------------------------------------- 1 | @import '../global.less'; 2 | 3 | nav.menu { 4 | grid-area: menu; 5 | display: grid; 6 | height: 100%; 7 | grid-template-columns: @height-menu-category-width auto; 8 | } 9 | 10 | nav.menu ul { 11 | margin: 0; 12 | padding: 0; 13 | list-style-type: none; 14 | } 15 | 16 | nav.menu div.icons { 17 | background-color: @colour-menu-main-background; 18 | position: relative; 19 | } 20 | 21 | nav.menu div.icons ul.bottom { 22 | position: absolute; 23 | bottom: 6px; 24 | width: 100%; 25 | } 26 | 27 | nav.menu div.icons ul li { 28 | display: block; 29 | width: 100%; 30 | height: @height-menu-category-width; 31 | -moz-box-sizing: border-box; 32 | -webkit-box-sizing: border-box; 33 | box-sizing: border-box; 34 | padding: 8px; 35 | } 36 | 37 | nav.menu div.icons ul li.big { 38 | padding: 5px; 39 | } 40 | 41 | nav.menu div.icons ul.top li:first-child { 42 | margin-top: calc(@height-menu-margin-top - 5px); 43 | } 44 | 45 | nav.menu div.icons ul li a { 46 | display: block; 47 | width: 100%; 48 | height: 100%; 49 | line-height: 0; 50 | font-size: 0; 51 | color: transparent; 52 | } 53 | 54 | nav.menu div.sub { 55 | padding: @height-menu-margin-top 0 0 10px; 56 | background-color: @colour-menu-sub-background; 57 | background-image: linear-gradient(to right, @colour-menu-sub-background, darken(@colour-menu-sub-background, 20%)); 58 | background-size: @width-menu-gradient 100%; 59 | background-position: right; 60 | background-repeat: no-repeat; 61 | } 62 | 63 | nav.menu div.sub h3 { 64 | font-size: 20px; 65 | font-weight: 500; 66 | margin: 0; 67 | padding: 0; 68 | } 69 | 70 | nav.menu div.sub h4 { 71 | font-size: 14px; 72 | font-weight: 500; 73 | letter-spacing: 2px; 74 | text-transform: uppercase; 75 | margin: 15px 0 5px 0; 76 | padding: 0; 77 | color: @colour-menu-sub-h4-colour; 78 | } 79 | 80 | nav.menu div.sub ul { 81 | margin: 0; 82 | padding: 0; 83 | list-style-type: none; 84 | } 85 | 86 | nav.menu div.sub ul li { 87 | margin-bottom: 5px; 88 | } 89 | 90 | nav.menu div.sub ul li:last-child { 91 | margin: 0; 92 | } 93 | 94 | nav.menu div.sub ul li a { 95 | display: block; 96 | width: 100%; 97 | height: 100%; 98 | padding: 6px 0 6px; 99 | font-size: 15px; 100 | color: @colour-menu-li-a-hover; 101 | text-decoration: none; 102 | background-size: 75% 75% !important; 103 | background-position: -94px; 104 | } 105 | 106 | nav.menu div.sub ul li a:hover { 107 | color: @colour-menu-li-a-hover-text; 108 | background-color: @colour-menu-li-a-hover; 109 | background-size: 75% 75%, @width-menu-gradient 100%; 110 | background-position: -94px; 111 | border-top-left-radius: @radius; 112 | border-bottom-left-radius: @radius; 113 | } 114 | 115 | nav.menu div.sub ul li a span { 116 | display: inline-block; 117 | margin-left: 32px; 118 | } -------------------------------------------------------------------------------- /static/logui/less/nav/trail.less: -------------------------------------------------------------------------------- 1 | @import '../global.less'; 2 | 3 | nav.trail { 4 | background-color: @colour-trail-background; 5 | line-height: @height-row-top; 6 | margin-left: 20px; 7 | } 8 | 9 | nav.trail ul { 10 | margin: 0; 11 | padding: 0; 12 | list-style-type: none; 13 | } 14 | 15 | nav.trail ul li { 16 | display: inline; 17 | } 18 | 19 | nav.trail ul li a { 20 | color: @colour-trail-link; 21 | text-decoration: none; 22 | } 23 | 24 | nav.trail ul li a:hover { 25 | color: @colour-trail-link-hover; 26 | text-decoration: underline; 27 | } 28 | 29 | nav.trail ul li a strong { 30 | font-weight: 500; 31 | } 32 | 33 | nav.trail ul li:after { 34 | position: relative; 35 | bottom: 2px; 36 | padding: 0 5px 0 5px; 37 | font-size: 8pt; 38 | content: ' \27A4'; 39 | } 40 | 41 | nav.trail ul li:last-child:after { 42 | content: ''; 43 | } -------------------------------------------------------------------------------- /static/logui/less/roboto.less: -------------------------------------------------------------------------------- 1 | @font-face { 2 | font-family: 'Roboto'; 3 | font-style: normal; 4 | font-weight: 300; 5 | font-display: swap; 6 | src: url(../../fonts/roboto-300-le.woff2) format('woff2'); 7 | unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF; 8 | } 9 | 10 | @font-face { 11 | font-family: 'Roboto'; 12 | font-style: normal; 13 | font-weight: 300; 14 | font-display: swap; 15 | src: url(../../fonts/roboto-300-l.woff2) format('woff2'); 16 | unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; 17 | } 18 | 19 | 20 | @font-face { 21 | font-family: 'Roboto'; 22 | font-style: normal; 23 | font-weight: 400; 24 | font-display: swap; 25 | src: url(../../fonts/roboto-400-le.woff2) format('woff2'); 26 | unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF; 27 | } 28 | 29 | @font-face { 30 | font-family: 'Roboto'; 31 | font-style: normal; 32 | font-weight: 400; 33 | font-display: swap; 34 | src: url(../../fonts/roboto-400-l.woff2) format('woff2'); 35 | unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; 36 | } 37 | 38 | 39 | @font-face { 40 | font-family: 'Roboto'; 41 | font-style: normal; 42 | font-weight: 500; 43 | font-display: swap; 44 | src: url(../../fonts/roboto-500-le.woff2) format('woff2'); 45 | unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF; 46 | } 47 | 48 | @font-face { 49 | font-family: 'Roboto'; 50 | font-style: normal; 51 | font-weight: 500; 52 | font-display: swap; 53 | src: url(../../fonts/roboto-500-l.woff2) format('woff2'); 54 | unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; 55 | } 56 | 57 | 58 | @font-face { 59 | font-family: 'Roboto'; 60 | font-style: normal; 61 | font-weight: 700; 62 | font-display: swap; 63 | src: url(../../fonts/roboto-700-le.woff2) format('woff2'); 64 | unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF; 65 | } 66 | 67 | @font-face { 68 | font-family: 'Roboto'; 69 | font-style: normal; 70 | font-weight: 700; 71 | font-display: swap; 72 | src: url(../../fonts/roboto-700-l.woff2) format('woff2'); 73 | unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; 74 | } -------------------------------------------------------------------------------- /static/logui/less/session.less: -------------------------------------------------------------------------------- 1 | @import 'global.less'; 2 | 3 | main section div.table.session div.row { 4 | grid-template-columns: 2fr 1fr 1fr 40px 40px 40px 40px; 5 | } 6 | 7 | main section div.table.session div.row span.browser { 8 | position: relative; 9 | right: 5px; 10 | } -------------------------------------------------------------------------------- /worker/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "@babel/preset-env", 4 | "@babel/preset-react" 5 | ], 6 | 7 | "plugins": [ 8 | [ 9 | "@babel/plugin-proposal-class-properties", 10 | { 11 | "loose": true 12 | } 13 | ], 14 | [ 15 | "@babel/plugin-transform-classes", 16 | { 17 | "loose": true 18 | } 19 | ] 20 | ] 21 | } -------------------------------------------------------------------------------- /worker/Dockerfile: -------------------------------------------------------------------------------- 1 | # 2 | # LogUI Server HTTP Worker Process Dockerfile 3 | # Replace the CMD to launch the Websocket worker instead. 4 | # 5 | # Author: David Maxwell 6 | # Date: 2021-03-23 7 | # 8 | 9 | FROM python:3.8-alpine 10 | 11 | LABEL maintainer="maxwelld90@acm.org" 12 | 13 | ARG SECRET_KEY 14 | 15 | RUN apk add --update --no-cache \ 16 | gcc \ 17 | libc-dev \ 18 | libffi-dev \ 19 | libressl-dev \ 20 | musl-dev \ 21 | postgresql-dev \ 22 | nodejs \ 23 | npm 24 | 25 | RUN mkdir -p /logui/worker/ 26 | WORKDIR /logui/worker 27 | 28 | RUN npm install -g @babel/core @babel/cli browserify less uglify-js 29 | 30 | COPY ./requirements/requirements.txt /logui/worker/requirements.txt 31 | RUN pip install -r /logui/worker/requirements.txt --trusted-host pypi.org --trusted-host pypi.python.org --trusted-host=files.pythonhosted.org 32 | 33 | COPY ./worker/package.json /logui/worker/package.json 34 | COPY ./worker/package-lock.json /logui/worker/package-lock.json 35 | RUN npm install 36 | 37 | COPY ./worker /logui/worker 38 | COPY ./app /logui/worker/app 39 | COPY ./static /logui/worker/copied-static 40 | 41 | CMD ["/logui/worker/http-entrypoint.sh"] -------------------------------------------------------------------------------- /worker/http-entrypoint.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # 4 | # LogUI Server HTTP Worker Entrypoint Script 5 | # 6 | # Author: David Maxwell 7 | # Date: 2021-03-23 8 | # 9 | 10 | 11 | echo "Starting up HTTP worker..." 12 | 13 | echo "Collecting static files..." 14 | python manage.py collectstatic --noinput 15 | 16 | echo "Compressing static files (building app)..." 17 | python manage.py compress 18 | 19 | echo "Migrating database..." 20 | python manage.py makemigrations 21 | python manage.py migrate 22 | 23 | echo "LogUI HTTP server is running" 24 | gunicorn worker.wsgi:logui_application -b 0.0.0.0:8000 -w 4 -------------------------------------------------------------------------------- /worker/logui_apps/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/logui-framework/server/b6635943865a71f8b9e2dcc2aaafabe5bbd7ff83/worker/logui_apps/__init__.py -------------------------------------------------------------------------------- /worker/logui_apps/control/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/logui-framework/server/b6635943865a71f8b9e2dcc2aaafabe5bbd7ff83/worker/logui_apps/control/__init__.py -------------------------------------------------------------------------------- /worker/logui_apps/control/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | from . import models 3 | 4 | class FlightAdminDisplay(admin.ModelAdmin): 5 | list_display = ('name', 'application', 'fqdn') 6 | 7 | class SessionAdminDisplay(admin.ModelAdmin): 8 | list_display = ('flight', 'server_start_timestamp', 'ip_address') 9 | ordering = ('-server_start_timestamp', 'flight') 10 | 11 | admin.site.register(models.Application) 12 | admin.site.register(models.Flight, FlightAdminDisplay) 13 | admin.site.register(models.Session, SessionAdminDisplay) -------------------------------------------------------------------------------- /worker/logui_apps/control/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class ControlConfig(AppConfig): 5 | name = 'control' 6 | -------------------------------------------------------------------------------- /worker/logui_apps/control/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/logui-framework/server/b6635943865a71f8b9e2dcc2aaafabe5bbd7ff83/worker/logui_apps/control/migrations/__init__.py -------------------------------------------------------------------------------- /worker/logui_apps/control/models.py: -------------------------------------------------------------------------------- 1 | import uuid 2 | from django.db import models 3 | from django.utils import timezone 4 | from django.contrib.auth.models import User 5 | 6 | 7 | class Application(models.Model): 8 | id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) 9 | created_by = models.ForeignKey(User, on_delete=models.RESTRICT) 10 | name = models.CharField(max_length=256, unique=True) 11 | creation_timestamp = models.DateTimeField(default=timezone.now) 12 | 13 | class Meta: 14 | verbose_name_plural = 'Applications' 15 | 16 | def __str__(self): 17 | return self.name 18 | 19 | 20 | class Flight(models.Model): 21 | id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) 22 | created_by = models.ForeignKey(User, on_delete=models.RESTRICT) 23 | application = models.ForeignKey(Application, on_delete=models.CASCADE) 24 | name = models.CharField(max_length=256, unique=True) 25 | is_active = models.BooleanField(default=True) 26 | fqdn = models.CharField(max_length=1024) 27 | creation_timestamp = models.DateTimeField(default=timezone.now) 28 | 29 | class Meta: 30 | verbose_name_plural = 'Flights' 31 | 32 | def __str__(self): 33 | return f'{self.name} ({self.application.name})' 34 | 35 | 36 | class Session(models.Model): 37 | id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) 38 | flight = models.ForeignKey(Flight, on_delete=models.CASCADE) 39 | is_active = models.BooleanField(default=True) 40 | ip_address = models.GenericIPAddressField() 41 | ua_string = models.TextField() 42 | server_start_timestamp = models.DateTimeField(default=timezone.now) 43 | server_end_timestamp = models.DateTimeField(null=True, blank=True) 44 | client_start_timestamp = models.DateTimeField() 45 | client_end_timestamp = models.DateTimeField(null=True, blank=True) 46 | 47 | 48 | class Meta: 49 | verbose_name_plural = 'Sessions' 50 | 51 | def __str__(self): 52 | return f'{self.flight.application.name}, Flight {self.flight.name}, Session {self.id}' -------------------------------------------------------------------------------- /worker/logui_apps/control/templates/logui/control/landing.html: -------------------------------------------------------------------------------- 1 | {% extends 'logui/base.html' %} 2 | 3 | {% load static %} 4 | {% load compress %} 5 | 6 | {% block cssblock %} 7 | {% compress css %} 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | {% endcompress %} 22 | {% endblock %} 23 | 24 | {% block jsblock %} 25 | 29 | {% compress js %} 30 | 31 | {% endcompress %} 32 | {% endblock %} -------------------------------------------------------------------------------- /worker/logui_apps/control/tests.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | # Create your tests here. 4 | -------------------------------------------------------------------------------- /worker/logui_apps/control/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import path 2 | from . import views 3 | 4 | app_name = 'control' 5 | 6 | urlpatterns = [ 7 | path('', views.LandingView.as_view(), name='landing') 8 | ] -------------------------------------------------------------------------------- /worker/logui_apps/control/views.py: -------------------------------------------------------------------------------- 1 | from django.views import View 2 | from django.shortcuts import render 3 | 4 | class LandingView(View): 5 | def get(self, request): 6 | return render(request, 'logui/control/landing.html', {}) -------------------------------------------------------------------------------- /worker/logui_apps/control_api/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/logui-framework/server/b6635943865a71f8b9e2dcc2aaafabe5bbd7ff83/worker/logui_apps/control_api/__init__.py -------------------------------------------------------------------------------- /worker/logui_apps/control_api/application/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/logui-framework/server/b6635943865a71f8b9e2dcc2aaafabe5bbd7ff83/worker/logui_apps/control_api/application/__init__.py -------------------------------------------------------------------------------- /worker/logui_apps/control_api/application/serializers.py: -------------------------------------------------------------------------------- 1 | from rest_framework import serializers 2 | from ...control.models import Application, Flight 3 | from ..utils import get_split_timestamp 4 | 5 | class ApplicationSerializer(serializers.ModelSerializer): 6 | flights = serializers.SerializerMethodField(read_only=True) 7 | creation_timestamp_split = serializers.SerializerMethodField() 8 | 9 | def get_creation_timestamp_split(self, obj): 10 | return get_split_timestamp(obj.creation_timestamp) 11 | 12 | def get_flights(self, application_reference): 13 | return Flight.objects.filter(application=application_reference).count() 14 | 15 | class Meta: 16 | model = Application 17 | fields = '__all__' 18 | 19 | class NewApplicationSerializer(serializers.ModelSerializer): 20 | 21 | class Meta: 22 | model = Application 23 | fields = ('name', 'created_by') -------------------------------------------------------------------------------- /worker/logui_apps/control_api/application/views.py: -------------------------------------------------------------------------------- 1 | from rest_framework.views import APIView 2 | from rest_framework.response import Response 3 | from rest_framework import permissions, status 4 | 5 | from ...control.models import Application 6 | from .serializers import ApplicationSerializer, NewApplicationSerializer 7 | 8 | 9 | class ApplicationInfoView(APIView): 10 | permission_classes = (permissions.IsAuthenticated,) 11 | 12 | def get(self, request, appID=None): 13 | applications = Application.objects.all() 14 | many = True 15 | 16 | try: 17 | if appID is not None: 18 | applications = Application.objects.get(id=appID) 19 | many = False 20 | except Application.DoesNotExist: 21 | return Response("", status=status.HTTP_400_BAD_REQUEST) 22 | 23 | serializer = ApplicationSerializer(applications, many=many) 24 | return Response(serializer.data, status=status.HTTP_200_OK) 25 | 26 | 27 | class CheckApplicationNameView(APIView): 28 | permission_classes = (permissions.IsAuthenticated,) 29 | 30 | def get(self, request): 31 | new_name = request.GET.get('name') 32 | response_dict = { 33 | 'name': new_name, 34 | 'is_available': True, 35 | } 36 | 37 | try: 38 | application = Application.objects.get(name__iexact=new_name) 39 | response_dict['is_available'] = False 40 | except Application.DoesNotExist: 41 | pass 42 | 43 | return Response(response_dict, status=status.HTTP_200_OK) 44 | 45 | 46 | class AddApplicationView(APIView): 47 | permission_classes = (permissions.IsAuthenticated,) 48 | 49 | def post(self, request): 50 | if 'name' not in request.data: 51 | return Response({}, status=status.HTTP_400_BAD_REQUEST) 52 | 53 | data = {} 54 | data['name'] = request.data['name'] 55 | data['created_by'] = request.user.id 56 | 57 | if (data['name'] == ''): 58 | return Response({}, status=status.HTTP_400_BAD_REQUEST) 59 | 60 | try: 61 | application = Application.objects.get(name__iexact=data['name']) 62 | return Response({}, status=status.HTTP_409_CONFLICT) 63 | except Application.DoesNotExist: 64 | pass 65 | 66 | serializer = NewApplicationSerializer(data=data) 67 | 68 | if serializer.is_valid(): 69 | serializer.save() 70 | return Response({}, status=status.HTTP_201_CREATED) 71 | 72 | return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) -------------------------------------------------------------------------------- /worker/logui_apps/control_api/flight/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/logui-framework/server/b6635943865a71f8b9e2dcc2aaafabe5bbd7ff83/worker/logui_apps/control_api/flight/__init__.py -------------------------------------------------------------------------------- /worker/logui_apps/control_api/flight/serializers.py: -------------------------------------------------------------------------------- 1 | from rest_framework import serializers 2 | from ..application.serializers import ApplicationSerializer 3 | from ...control.models import Application, Flight, Session 4 | from ..utils import get_split_timestamp 5 | 6 | class FlightSerializer(serializers.ModelSerializer): 7 | sessions = serializers.SerializerMethodField(read_only=True) 8 | application = ApplicationSerializer(many=False, read_only=True) 9 | creation_timestamp_split = serializers.SerializerMethodField() 10 | 11 | def get_creation_timestamp_split(self, obj): 12 | return get_split_timestamp(obj.creation_timestamp) 13 | 14 | def get_sessions(self, flight_reference): 15 | return Session.objects.filter(flight=flight_reference).count() 16 | 17 | class Meta: 18 | model = Flight 19 | fields = '__all__' 20 | 21 | 22 | class NewFlightSerializer(serializers.ModelSerializer): 23 | 24 | class Meta: 25 | model = Flight 26 | fields = ('name', 'fqdn', 'created_by', 'application') -------------------------------------------------------------------------------- /worker/logui_apps/control_api/flight/views.py: -------------------------------------------------------------------------------- 1 | import io 2 | import os 3 | import json 4 | from django.core import signing 5 | from django.http import StreamingHttpResponse 6 | from ...control.models import Application, Flight 7 | from .serializers import FlightSerializer, NewFlightSerializer 8 | from mongo import get_mongo_connection_handle, get_mongo_collection_handle 9 | 10 | from rest_framework.views import APIView 11 | from rest_framework.response import Response 12 | from rest_framework import permissions, status 13 | 14 | class FlightInfo(APIView): 15 | permission_classes = (permissions.IsAuthenticated,) 16 | 17 | def get(self, request, appID=None): 18 | many = True 19 | 20 | if appID is None: 21 | return Response("", status=status.HTTP_400_BAD_REQUEST) 22 | 23 | try: 24 | application = Application.objects.get(id=appID) 25 | except Application.DoesNotExist: 26 | return Response("", status=status.HTTP_400_BAD_REQUEST) 27 | 28 | flights = Flight.objects.filter(application=application) 29 | 30 | serializer = FlightSerializer(flights, many=many) 31 | return Response(serializer.data, status=status.HTTP_200_OK) 32 | 33 | 34 | class SpecificFlightInfoView(APIView): 35 | permission_classes = (permissions.IsAuthenticated,) 36 | 37 | def get(self, request, flightID=None): 38 | if flightID is None: 39 | return Response("", status=status.HTTP_400_BAD_REQUEST) 40 | 41 | try: 42 | flight = Flight.objects.get(id=flightID) 43 | except Flight.DoesNotExist: 44 | return Response("", status=status.HTTP_400_BAD_REQUEST) 45 | 46 | serializer = FlightSerializer(flight, many=False) 47 | return Response(serializer.data, status=status.HTTP_200_OK) 48 | 49 | 50 | class FlightAuthorisationTokenView(APIView): 51 | permission_classes = (permissions.IsAuthenticated,) 52 | 53 | def get_authorisation_object(self, flight): 54 | return { 55 | 'type': 'logUI-authorisation-object', 56 | 'applicationID': str(flight.application.id), 57 | 'flightID': str(flight.id), 58 | } 59 | 60 | def get(self, request, flightID=None): 61 | if flightID is None: 62 | return Response("", status=status.HTTP_400_BAD_REQUEST) 63 | 64 | try: 65 | flight = Flight.objects.get(id=flightID) 66 | except Flight.DoesNotExist: 67 | return Response("", status=status.HTTP_400_BAD_REQUEST) 68 | 69 | response_dict = { 70 | 'flightID': str(flight.id), 71 | 'flightAuthorisationToken': signing.dumps(self.get_authorisation_object(flight)), 72 | } 73 | 74 | return Response(response_dict, status=status.HTTP_200_OK) 75 | 76 | 77 | class FlightStatusView(APIView): 78 | permission_classes = (permissions.IsAuthenticated,) 79 | 80 | def get_flight_object(self, flightID): 81 | try: 82 | flight = Flight.objects.get(id=flightID) 83 | return flight 84 | except Flight.DoesNotExist: 85 | pass 86 | 87 | return False 88 | 89 | def get(self, request, flightID=None): 90 | flight = self.get_flight_object(flightID) 91 | 92 | if flight: 93 | return Response({'flightID': flightID, 'is_active': flight.is_active}, status=status.HTTP_200_OK) 94 | 95 | return Response({}, status=status.HTTP_404_NOT_FOUND) 96 | 97 | def patch(self, request, flightID=None): 98 | flight = self.get_flight_object(flightID) 99 | 100 | if flight: 101 | flight.is_active = not flight.is_active 102 | flight.save() 103 | 104 | return Response({'flightID': flightID, 'is_active': flight.is_active}, status=status.HTTP_200_OK) 105 | 106 | return Response({}, status=status.HTTP_404_NOT_FOUND) 107 | 108 | 109 | class CheckFlightNameView(APIView): 110 | permission_classes = (permissions.IsAuthenticated,) 111 | 112 | def get(self, request, appID): 113 | new_name = request.GET.get('flightName') 114 | response_dict = { 115 | 'flightName': new_name, 116 | 'is_available': True, 117 | } 118 | 119 | if new_name is None: 120 | return Response({}, status=status.HTTP_400_BAD_REQUEST) 121 | 122 | try: 123 | application = Application.objects.get(id=appID) 124 | except Application.DoesNotExist: 125 | return Response({}, status=status.HTTP_400_BAD_REQUEST) 126 | 127 | try: 128 | flight = Flight.objects.get(name__iexact=new_name, application=application) 129 | response_dict['is_available'] = False 130 | except Flight.DoesNotExist: 131 | pass 132 | 133 | return Response(response_dict, status=status.HTTP_200_OK) 134 | 135 | 136 | class AddFlightView(APIView): 137 | permission_classes = (permissions.IsAuthenticated,) 138 | 139 | def post(self, request, appID): 140 | if 'flightName' not in request.data or 'fqdn' not in request.data: 141 | return Response({}, status=status.HTTP_400_BAD_REQUEST) 142 | 143 | try: 144 | application = Application.objects.get(id=appID) 145 | except Application.DoesNotExist: 146 | return Response({}, status=status.HTTP_400_BAD_REQUEST) 147 | 148 | try: 149 | flight = Flight.objects.get(name__iexact=request.data['flightName'], application=application) 150 | return Response({}, status=status.HTTP_409_CONFLICT) 151 | except Flight.DoesNotExist: 152 | pass 153 | 154 | data = {} 155 | data['name'] = request.data['flightName'] 156 | data['fqdn'] = request.data['fqdn'] 157 | data['created_by'] = request.user.id 158 | data['application'] = application.id 159 | 160 | serializer = NewFlightSerializer(data=data) 161 | 162 | if serializer.is_valid(): 163 | serializer.save() 164 | return Response({}, status=status.HTTP_201_CREATED) 165 | 166 | return Response({}, status=status.HTTP_201_CREATED) 167 | 168 | class FlightLogDataDownloaderView(APIView): 169 | permission_classes = (permissions.IsAuthenticated,) 170 | 171 | def get(self, request, flightID): 172 | 173 | try: 174 | flight = Flight.objects.get(id=flightID) 175 | except Flight.DoesNotExist: 176 | return Response({}, status=status.HTTP_404_NOT_FOUND) 177 | 178 | mongo_db_handle, mongo_connection = get_mongo_connection_handle() 179 | 180 | # Do we have a collection for the flight in the MongoDB instance? 181 | # If not, this means the flight has been created, but no data yet exists for it. 182 | if not str(flight.id) in mongo_db_handle.list_collection_names(): 183 | return Response({}, status=status.HTTP_204_NO_CONTENT) 184 | 185 | # If we get here, then there is a collection -- and we can get the data for it. 186 | mongo_collection_handle = get_mongo_collection_handle(mongo_db_handle, str(flight.id)) 187 | 188 | # Get all of the data. 189 | # This also omits the _id field that is added by MongoDB -- we don't need it. 190 | log_entries = mongo_collection_handle.find({}, {'_id': False}) 191 | stream = io.StringIO() 192 | 193 | stream.write(f'[{os.linesep}{os.linesep}') 194 | 195 | # Get the count and if it matches the length... 196 | no_entries = log_entries.count() 197 | counter = 0 198 | 199 | for entry in log_entries: 200 | if counter == (no_entries - 1): 201 | stream.write(f'{json.dumps(entry)}{os.linesep}{os.linesep}') 202 | continue 203 | 204 | stream.write(f'{json.dumps(entry)},{os.linesep}{os.linesep}') 205 | counter += 1 206 | 207 | stream.write(f']') 208 | stream.seek(0) 209 | 210 | response = StreamingHttpResponse(stream, content_type='application/json') 211 | response['Content-Disposition'] = f'attachment; filename=logui-{str(flight.id)}.log' 212 | 213 | mongo_connection.close() 214 | return response -------------------------------------------------------------------------------- /worker/logui_apps/control_api/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/logui-framework/server/b6635943865a71f8b9e2dcc2aaafabe5bbd7ff83/worker/logui_apps/control_api/migrations/__init__.py -------------------------------------------------------------------------------- /worker/logui_apps/control_api/serializers.py: -------------------------------------------------------------------------------- 1 | from rest_framework import serializers 2 | from rest_framework_jwt.settings import api_settings 3 | from django.contrib.auth.models import User 4 | 5 | from ..control.models import Application 6 | 7 | class ApplicationSerializer(serializers.ModelSerializer): 8 | class Meta: 9 | model = Application 10 | fields = '__all__' 11 | 12 | class UserSerializer(serializers.ModelSerializer): 13 | class Meta: 14 | model = User 15 | fields = ['username', 'first_name', 'last_name'] 16 | 17 | 18 | class UserSerializerWithToken(serializers.ModelSerializer): 19 | token = serializers.SerializerMethodField() 20 | password = serializers.CharField(write_only=True) 21 | 22 | def get_token(self, obj): 23 | jwt_payload_handler = api_settings.JWT_PAYLOAD_HANDLER 24 | jwt_encode_handler = api_settings.JWT_ENCODE_HANDLER 25 | 26 | payload = jwt_payload_handler(obj) 27 | token = jwt_encode_handler(payload) 28 | 29 | return token 30 | 31 | def create(self, validate_data): 32 | password = validated_data.pop('password', None) 33 | instance = self.Meta.model(**validated_data) 34 | 35 | if password is not None: 36 | instance.set_password(password) 37 | 38 | instance.save() 39 | 40 | return instance 41 | 42 | class Meta: 43 | model = User 44 | fields = ('token', 'username', 'password') -------------------------------------------------------------------------------- /worker/logui_apps/control_api/session/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/logui-framework/server/b6635943865a71f8b9e2dcc2aaafabe5bbd7ff83/worker/logui_apps/control_api/session/__init__.py -------------------------------------------------------------------------------- /worker/logui_apps/control_api/session/serializers.py: -------------------------------------------------------------------------------- 1 | from rest_framework import serializers 2 | from ...control.models import Session 3 | from ..application.serializers import ApplicationSerializer 4 | from ..flight.serializers import FlightSerializer 5 | from ..utils import get_split_timestamp, get_ua_details 6 | 7 | class SessionSerializer(serializers.ModelSerializer): 8 | application = ApplicationSerializer(many=False, read_only=True) 9 | flight = FlightSerializer(many=False, read_only=True) 10 | split_timestamps = serializers.SerializerMethodField() 11 | agent_details = serializers.SerializerMethodField() 12 | 13 | def get_split_timestamps(self, obj): 14 | return { 15 | 'start_timestamp': get_split_timestamp(obj.server_start_timestamp), 16 | 'end_timestamp': get_split_timestamp(obj.server_end_timestamp) 17 | } 18 | 19 | def get_agent_details(self, obj): 20 | return get_ua_details(obj.ua_string) 21 | 22 | class Meta: 23 | model = Session 24 | exclude = ('ua_string',) -------------------------------------------------------------------------------- /worker/logui_apps/control_api/session/views.py: -------------------------------------------------------------------------------- 1 | from rest_framework.views import APIView 2 | from rest_framework.response import Response 3 | from rest_framework import permissions, status 4 | 5 | from ...control.models import Flight, Session 6 | from .serializers import SessionSerializer 7 | 8 | 9 | class SessionListView(APIView): 10 | permission_classes = (permissions.IsAuthenticated,) 11 | 12 | def get(self, request, flightID=None): 13 | many = True 14 | 15 | if flightID is None: 16 | return Response("", status=status.HTTP_400_BAD_REQUEST) 17 | 18 | try: 19 | flight = Flight.objects.get(id=flightID) 20 | except Flight.DoesNotExist: 21 | return Response("", status=status.HTTP_400_BAD_REQUEST) 22 | 23 | sessions = Session.objects.filter(flight=flightID) 24 | 25 | serializer = SessionSerializer(sessions, many=many) 26 | return Response(serializer.data, status=status.HTTP_200_OK) 27 | 28 | 29 | class SessionInfoView(APIView): 30 | permission_classes = (permissions.IsAuthenticated,) 31 | 32 | def get(self, request, sessionID=None): 33 | many = True 34 | 35 | if sessionID is None: 36 | return Response("", status=status.HTTP_400_BAD_REQUEST) 37 | 38 | try: 39 | session = Session.objects.get(id=sessionID) 40 | except Session.DoesNotExist: 41 | return Response("", status=status.HTTP_400_BAD_REQUEST) 42 | 43 | serializer = SessionSerializer(session, many=False) 44 | return Response(serializer.data, status=status.HTTP_200_OK) -------------------------------------------------------------------------------- /worker/logui_apps/control_api/urls.py: -------------------------------------------------------------------------------- 1 | from rest_framework_jwt.views import obtain_jwt_token 2 | from django.urls import path 3 | 4 | from .user import views as user_views 5 | from .application import views as application_views 6 | from .flight import views as flight_views 7 | from .session import views as session_views 8 | 9 | app_name = 'control_api' 10 | 11 | urlpatterns = [ 12 | path('user/auth/', obtain_jwt_token), 13 | path('user/current/', user_views.CurrentUser.as_view()), 14 | path('user/create/', user_views.CreateUserAccount.as_view()), # This mapping is for when we wish to create new user accounts. 15 | 16 | path('application/list/', application_views.ApplicationInfoView.as_view()), 17 | path('application/specific//', application_views.ApplicationInfoView.as_view()), 18 | path('application/add/check/', application_views.CheckApplicationNameView.as_view()), 19 | path('application/add/', application_views.AddApplicationView.as_view()), 20 | 21 | path('flight/list//', flight_views.FlightInfo.as_view()), 22 | path('flight//add/check/', flight_views.CheckFlightNameView.as_view()), 23 | path('flight//add/', flight_views.AddFlightView.as_view()), 24 | path('flight/info//', flight_views.SpecificFlightInfoView.as_view()), 25 | path('flight/info//status/', flight_views.FlightStatusView.as_view()), 26 | path('flight/info//token/', flight_views.FlightAuthorisationTokenView.as_view()), 27 | path('flight/download//', flight_views.FlightLogDataDownloaderView.as_view()), 28 | 29 | path('session/list//', session_views.SessionListView.as_view()), 30 | path('session/info//', session_views.SessionInfoView.as_view()), 31 | ] -------------------------------------------------------------------------------- /worker/logui_apps/control_api/user/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/logui-framework/server/b6635943865a71f8b9e2dcc2aaafabe5bbd7ff83/worker/logui_apps/control_api/user/__init__.py -------------------------------------------------------------------------------- /worker/logui_apps/control_api/user/serializers.py: -------------------------------------------------------------------------------- 1 | from rest_framework import serializers 2 | from rest_framework_jwt.settings import api_settings 3 | from django.contrib.auth.models import User 4 | 5 | 6 | class UserSerializer(serializers.ModelSerializer): 7 | class Meta: 8 | model = User 9 | fields = ['username', 'first_name', 'last_name'] 10 | 11 | 12 | class UserSerializerWithToken(serializers.ModelSerializer): 13 | token = serializers.SerializerMethodField() 14 | password = serializers.CharField(write_only=True) 15 | 16 | def get_token(self, obj): 17 | jwt_payload_handler = api_settings.JWT_PAYLOAD_HANDLER 18 | jwt_encode_handler = api_settings.JWT_ENCODE_HANDLER 19 | 20 | payload = jwt_payload_handler(obj) 21 | token = jwt_encode_handler(payload) 22 | 23 | return token 24 | 25 | def create(self, validated_data): 26 | password = validated_data.pop('password', None) 27 | instance = self.Meta.model(**validated_data) 28 | 29 | if password is not None: 30 | instance.set_password(password) 31 | 32 | instance.save() 33 | 34 | return instance 35 | 36 | class Meta: 37 | model = User 38 | fields = ('token', 'username', 'password') -------------------------------------------------------------------------------- /worker/logui_apps/control_api/user/views.py: -------------------------------------------------------------------------------- 1 | from rest_framework.views import APIView 2 | from rest_framework.response import Response 3 | from rest_framework import permissions, status 4 | from .serializers import UserSerializer, UserSerializerWithToken 5 | 6 | 7 | class CurrentUser(APIView): 8 | permission_classes = (permissions.IsAuthenticated,) 9 | 10 | def get(self, request): 11 | serializer = UserSerializer(request.user) 12 | return Response(serializer.data, status=status.HTTP_200_OK) 13 | 14 | 15 | class CreateUserAccount(APIView): 16 | permission_classes = (permissions.AllowAny,) 17 | 18 | def post(self, request, format=None): 19 | serializer = UserSerializerWithToken(data=request.data) 20 | 21 | if serializer.is_valid(): 22 | serializer.save() 23 | return Response(serializer.data, status=status.HTTP_201_CREATED) 24 | 25 | return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) -------------------------------------------------------------------------------- /worker/logui_apps/control_api/utils.py: -------------------------------------------------------------------------------- 1 | from .serializers import UserSerializer 2 | from user_agents import parse 3 | 4 | def token_response_handler(token, user=None, request=None): 5 | return { 6 | 'token': token, 7 | 'user': UserSerializer(user, context={'request': request}).data, 8 | } 9 | 10 | def get_split_timestamp(datetime_object): 11 | return_dict = {} 12 | 13 | if datetime_object is None: 14 | return None 15 | 16 | return_dict['date'] = {} 17 | return_dict['time'] = {} 18 | 19 | return_dict['date']['friendly'] = datetime_object.strftime('%d %B %Y') 20 | return_dict['date']['locale'] = datetime_object.strftime('%x') 21 | 22 | return_dict['time']['locale'] = datetime_object.strftime('%X') 23 | 24 | return return_dict 25 | 26 | def get_ua_details(ua_string): 27 | ua_object = parse(ua_string) 28 | 29 | return_dict = {} 30 | 31 | return_dict['os'] = {} 32 | return_dict['os']['family'] = ua_object.os.family 33 | return_dict['os']['version'] = ua_object.os.version 34 | return_dict['os']['version_string'] = ua_object.os.version_string 35 | 36 | return_dict['browser'] = {} 37 | return_dict['browser']['family'] = ua_object.browser.family 38 | return_dict['browser']['version'] = ua_object.browser.version 39 | return_dict['browser']['version_string'] = ua_object.browser.version_string 40 | 41 | return_dict['is_desktop'] = ua_object.is_pc 42 | return_dict['is_touch_capable'] = ua_object.is_touch_capable 43 | 44 | return return_dict -------------------------------------------------------------------------------- /worker/logui_apps/errorhandling/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/logui-framework/server/b6635943865a71f8b9e2dcc2aaafabe5bbd7ff83/worker/logui_apps/errorhandling/__init__.py -------------------------------------------------------------------------------- /worker/logui_apps/errorhandling/views.py: -------------------------------------------------------------------------------- 1 | from django.shortcuts import render 2 | 3 | def handler_404_error(request, *args, **kwargs): 4 | response = render(request, 'logui/errors/404.html') 5 | response.status_code = 404 6 | 7 | return response 8 | 9 | def handler_500_error(request, *args, **argv): 10 | response = render(request, 'logui/errors/500.html') 11 | response.status_code = 500 12 | 13 | return response -------------------------------------------------------------------------------- /worker/logui_apps/websocket/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/logui-framework/server/b6635943865a71f8b9e2dcc2aaafabe5bbd7ff83/worker/logui_apps/websocket/__init__.py -------------------------------------------------------------------------------- /worker/logui_apps/websocket/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | # Register your models here. 4 | -------------------------------------------------------------------------------- /worker/logui_apps/websocket/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class EndpointConfig(AppConfig): 5 | name = 'endpoint' 6 | -------------------------------------------------------------------------------- /worker/logui_apps/websocket/consumers/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/logui-framework/server/b6635943865a71f8b9e2dcc2aaafabe5bbd7ff83/worker/logui_apps/websocket/consumers/__init__.py -------------------------------------------------------------------------------- /worker/logui_apps/websocket/consumers/endpoint.py: -------------------------------------------------------------------------------- 1 | from ...control.models import Application, Flight, Session 2 | from mongo import get_mongo_connection_handle, get_mongo_collection_handle 3 | 4 | from channels.generic.websocket import JsonWebsocketConsumer 5 | from django.core.exceptions import ValidationError 6 | from dateutil import parser as date_parser 7 | from urllib.parse import urlparse 8 | from django.core import signing 9 | from datetime import datetime 10 | 11 | SUPPORTED_CLIENTS = ['0.5.1', '0.5.2', '0.5.3', '0.5.4'] 12 | KNOWN_REQUEST_TYPES = ['handshake', 'closedown', 'logEvents'] 13 | BAD_REQUEST_LIMIT = 3 14 | 15 | class EndpointConsumer(JsonWebsocketConsumer): 16 | 17 | def __init__(self, *args, **kwargs): 18 | super().__init__(*args, **kwargs) 19 | self._handshake_success = False 20 | self._client_ip = None 21 | self._application = None 22 | self._flight = None 23 | self._session = None 24 | self._session_created = None 25 | self._mongo_db_handle, self._mongo_connection = get_mongo_connection_handle() 26 | self._mongo_collection = None 27 | 28 | def connect(self): 29 | self._client_ip = self.scope['client'][0] # Is this the correct value? 30 | self.accept() 31 | 32 | def receive_json(self, request_dict): 33 | if not self.validate_request(request_dict) or not self.validate_handshake(request_dict): 34 | return 35 | 36 | if request_dict['type'] == 'handshake': 37 | self.send_json(self.generate_message_object('handshakeSuccess', { 38 | 'sessionID': str(self._session.id), 39 | 'clientStartTimestamp': str(self._session.client_start_timestamp), 40 | 'newSessionCreated': self._session_created, 41 | })) 42 | return 43 | 44 | type_redirection = { 45 | 'logEvents': self.handle_log_events, 46 | } 47 | 48 | type_redirection[request_dict['type']](request_dict) 49 | 50 | def disconnect(self, close_code): 51 | self._mongo_connection.close() 52 | pass 53 | 54 | def generate_message_object(self, message_type, payload): 55 | return { 56 | 'sender': 'logUIServer', 57 | 'type': message_type, 58 | 'payload': payload, 59 | } 60 | 61 | def validate_request(self, request_dict): 62 | bad_request = False 63 | 64 | if 'sender' not in request_dict or request_dict['sender'] != 'logUIClient': 65 | bad_request = True 66 | 67 | if ('type' not in request_dict or request_dict['type'] not in KNOWN_REQUEST_TYPES) and not bad_request: 68 | bad_request = True 69 | 70 | if bad_request: 71 | self.close(code=4001) 72 | return False 73 | 74 | return True 75 | 76 | 77 | def validate_handshake(self, request_dict): 78 | if not self._handshake_success: 79 | if request_dict['type'] == 'handshake': 80 | print(request_dict) 81 | if ('clientVersion' not in request_dict['payload'] or 82 | 'authorisationToken' not in request_dict['payload'] or 83 | 'pageOrigin' not in request_dict['payload'] or 84 | 'userAgent' not in request_dict['payload'] or 85 | 'clientTimestamp' not in request_dict['payload']): 86 | self.close(code=4002) 87 | return False 88 | 89 | # Do we support the version of the client with this server? 90 | matching_version = False 91 | 92 | for permitted_version in SUPPORTED_CLIENTS: 93 | if request_dict['payload']['clientVersion'].startswith(permitted_version): 94 | matching_version = True 95 | break 96 | 97 | if not matching_version: 98 | self.close(code=4003) 99 | return False 100 | 101 | try: 102 | if not self.is_authorisation_valid(signing.loads(request_dict['payload']['authorisationToken']), request_dict['payload']['pageOrigin']): 103 | return False 104 | except signing.BadSignature: 105 | self.close(code=4004) 106 | return False 107 | 108 | # Check the session ID is okay, if it exists. 109 | # If it doesn't, we create a new session. 110 | if not self.check_set_session(request_dict): 111 | self.close(code=4006) 112 | return False 113 | 114 | # Is the flight set to accept new clients? 115 | # If not, then we must reject the request. 116 | if not self._flight.is_active: 117 | self.close(code=4007) 118 | return False 119 | 120 | # If we get here intact, the handshake was a success. 121 | self._handshake_success = True 122 | else: 123 | self.close(code=4001) 124 | return False 125 | 126 | return True 127 | 128 | def is_authorisation_valid(self, authorisation_object, page_origin): 129 | if ('type' not in authorisation_object or 130 | 'applicationID' not in authorisation_object or 131 | 'flightID' not in authorisation_object): 132 | self.close(code=4004) 133 | return False 134 | 135 | if authorisation_object['type'] != 'logUI-authorisation-object': 136 | self.close(code=4004) 137 | return False 138 | 139 | # Check the application exists. Set the instance variable. 140 | try: 141 | self._application = Application.objects.get(id=authorisation_object['applicationID']) 142 | except Application.DoesNotExist: 143 | self.close(code=4004) 144 | return False 145 | 146 | # Check the flight exists. Set the instance variable. 147 | try: 148 | self._flight = Flight.objects.get(id=authorisation_object['flightID']) 149 | except Flight.DoesNotExist: 150 | self.close(code=4004) 151 | return False 152 | 153 | # Check the domain matches the expected value. Set the instance variable. 154 | if self._flight.fqdn.lower() == 'bypass' or urlparse(self._flight.fqdn).netloc == 'localhost': 155 | return True 156 | else: 157 | split_origin = urlparse(page_origin) 158 | 159 | if self._flight.fqdn != split_origin.netloc: 160 | self.close(code=4005) 161 | return False 162 | 163 | return True 164 | 165 | def check_set_session(self, request_dict): 166 | user_agent = request_dict['payload']['userAgent'] 167 | 168 | if 'sessionID' in request_dict['payload']: 169 | session_id = request_dict['payload']['sessionID'] 170 | 171 | try: 172 | session = Session.objects.get(id=session_id) 173 | except Session.DoesNotExist: 174 | return False 175 | except ValidationError: 176 | return False 177 | 178 | # If flights do not match, we aren't using the same flight as originally started used. 179 | if session.flight != self._flight: 180 | return False 181 | 182 | # If the UA strings do not match, we aren't using the same browser as originally used. 183 | if session.ua_string != user_agent: 184 | return False 185 | 186 | self._session = session 187 | self._session_created = False 188 | return True 189 | 190 | # Create a new session object. 191 | session = Session() 192 | session.flight = self._flight 193 | session.ip_address = self._client_ip 194 | session.ua_string = user_agent 195 | session.client_start_timestamp = date_parser.parse(request_dict['payload']['clientTimestamp']) 196 | 197 | session.save() 198 | self._session = session 199 | self._session_created = True 200 | 201 | return True 202 | 203 | def handle_log_events(self, request_dict): 204 | if not self._session: 205 | self.close(code=4006) 206 | return 207 | 208 | import json 209 | 210 | for item in request_dict['payload']['items']: 211 | if item['eventType'] == 'statusEvent' and item['eventDetails']['type'] == 'stopped': 212 | self._session.client_end_timestamp = date_parser.parse(item['timestamps']['eventTimestamp']) 213 | self._session.server_end_timestamp = datetime.now() 214 | self._session.save() 215 | 216 | item['applicationID'] = str(self._application.id) 217 | item['flightID'] = str(self._flight.id) 218 | 219 | if not self._mongo_collection: 220 | self._mongo_collection = get_mongo_collection_handle(self._mongo_db_handle, str(self._flight.id)) 221 | 222 | self._mongo_collection.insert(item) -------------------------------------------------------------------------------- /worker/logui_apps/websocket/consumers/interface.py: -------------------------------------------------------------------------------- 1 | from channels.generic.websocket import WebsocketConsumer 2 | 3 | class InterfaceConsumer(WebsocketConsumer): 4 | def connect(self): 5 | self.accept() 6 | 7 | def disconnect(self, close_mode): 8 | pass 9 | 10 | def receive(self, text_data): 11 | print(text_data) 12 | 13 | self.send(text_data='interface consumer') -------------------------------------------------------------------------------- /worker/logui_apps/websocket/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/logui-framework/server/b6635943865a71f8b9e2dcc2aaafabe5bbd7ff83/worker/logui_apps/websocket/migrations/__init__.py -------------------------------------------------------------------------------- /worker/logui_apps/websocket/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | 3 | # Create your models here. 4 | -------------------------------------------------------------------------------- /worker/logui_apps/websocket/routing.py: -------------------------------------------------------------------------------- 1 | from django.urls import path 2 | from .consumers import endpoint 3 | from .consumers import interface 4 | 5 | websocket_urlpatterns = [ 6 | path('ws/endpoint/', endpoint.EndpointConsumer.as_asgi()), 7 | path('ws/interface/', interface.InterfaceConsumer.as_asgi()), 8 | ] -------------------------------------------------------------------------------- /worker/logui_apps/websocket/tests.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | # Create your tests here. 4 | -------------------------------------------------------------------------------- /worker/logui_apps/websocket/views.py: -------------------------------------------------------------------------------- 1 | from django.shortcuts import render 2 | 3 | # Create your views here. 4 | -------------------------------------------------------------------------------- /worker/manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """Django's command-line utility for administrative tasks.""" 3 | import os 4 | import sys 5 | from worker.settings import set_django_settings_module 6 | 7 | 8 | def main(): 9 | """Run administrative tasks.""" 10 | set_django_settings_module() 11 | try: 12 | from django.core.management import execute_from_command_line 13 | except ImportError as exc: 14 | raise ImportError( 15 | "Couldn't import Django. Are you sure it's installed and " 16 | "available on your PYTHONPATH environment variable? Did you " 17 | "forget to activate a virtual environment?" 18 | ) from exc 19 | execute_from_command_line(sys.argv) 20 | 21 | 22 | if __name__ == '__main__': 23 | main() 24 | -------------------------------------------------------------------------------- /worker/mongo.py: -------------------------------------------------------------------------------- 1 | from pymongo import MongoClient 2 | from django.conf import settings 3 | 4 | def get_mongo_connection_handle(): 5 | client = MongoClient(host=settings.MONGO_HOST, 6 | port=int(settings.MONGO_PORT), 7 | username=settings.MONGO_USERNAME, 8 | password=settings.MONGO_PASSWORD) 9 | 10 | return (client[settings.MONGO_DB], client) 11 | 12 | def get_mongo_collection_handle(db_handle, collection_name): 13 | return db_handle[collection_name] -------------------------------------------------------------------------------- /worker/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "devDependencies": { 3 | "@babel/core": "^7.12.9", 4 | "@babel/plugin-proposal-class-properties": "^7.12.1", 5 | "@babel/plugin-transform-classes": "^7.12.1", 6 | "@babel/preset-env": "^7.12.7", 7 | "@babel/preset-react": "^7.12.7", 8 | "babelify": "^10.0.0", 9 | "prop-types": "^15.7.2", 10 | "react": "^17.0.1", 11 | "react-dom": "^17.0.1", 12 | "react-router-dom": "^5.2.0", 13 | "regenerator-runtime": "^0.13.7" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /worker/templates/logui/base.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | {% load static %} 4 | {% load compress %} 5 | 6 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | {% compress css %} 18 | 19 | {% endcompress %} 20 | 21 | {% compress css %} 22 | 23 | 24 | {% endcompress %} 25 | 26 | {% block cssblock %} 27 | {% endblock %} 28 | 29 | 30 | 31 | LogUI 32 | 33 | 34 | 35 |
    36 | {% block bodyblock %} 37 | {% include 'logui/nojs.html' %} 38 | {% endblock %} 39 |
    40 | 41 | 42 | {% block jsblock %} 43 | {% endblock %} 44 | 45 | -------------------------------------------------------------------------------- /worker/templates/logui/errors/404.html: -------------------------------------------------------------------------------- 1 | {% extends 'logui/base.html' %} 2 | {% load static %} 3 | 4 | {% block bodyblock %} 5 | 6 | 7 | LogUI Logo 8 |

    Not Found (HTTP 404)

    9 |

    10 | LogUI couldn't find the page you are looking for. 11 | Check the path you have provided, and try again. 12 |

    13 | {% endblock %} -------------------------------------------------------------------------------- /worker/templates/logui/errors/500.html: -------------------------------------------------------------------------------- 1 | {% extends 'logui/base.html' %} 2 | {% load static %} 3 | 4 | {% block bodyblock %} 5 | 6 | 7 | LogUI Logo 8 |

    Internal Server Error (HTTP 500)

    9 |

    10 | Something went wrong with the LogUI server. Sorry about this! 11 | If you're the administrator, check the server logs to see what has happened. 12 |

    13 | {% endblock %} -------------------------------------------------------------------------------- /worker/templates/logui/nojs.html: -------------------------------------------------------------------------------- 1 | {% load static %} 2 | {% load compress %} 3 | 4 | 5 | 6 | LogUI Logo 7 |

    JavaScript Disabled

    8 |

    In order to run the LogUI Control Application, you need to ensure that JavaScript is enabled in your browser. Once enabled, refresh this page.

    -------------------------------------------------------------------------------- /worker/worker/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/logui-framework/server/b6635943865a71f8b9e2dcc2aaafabe5bbd7ff83/worker/worker/__init__.py -------------------------------------------------------------------------------- /worker/worker/asgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | ASGI config for worker project. 3 | 4 | It exposes the ASGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/3.1/howto/deployment/asgi/ 8 | """ 9 | 10 | import os 11 | import django 12 | from worker.settings import set_django_settings_module 13 | 14 | set_django_settings_module() 15 | django.setup() 16 | 17 | from channels.auth import AuthMiddlewareStack 18 | from channels.routing import ProtocolTypeRouter, URLRouter 19 | from django.core.asgi import get_asgi_application 20 | from logui_apps.websocket import routing 21 | 22 | logui_application = ProtocolTypeRouter({ 23 | 'websocket': AuthMiddlewareStack( 24 | URLRouter( 25 | routing.websocket_urlpatterns 26 | ) 27 | ), 28 | 'http': get_asgi_application() # This may not be required in production. 29 | }) 30 | -------------------------------------------------------------------------------- /worker/worker/settings/__init__.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | def set_django_settings_module(): 4 | if os.getenv('LOGUI_DEV'): 5 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'worker.settings.development') 6 | else: 7 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'worker.settings.docker') -------------------------------------------------------------------------------- /worker/worker/settings/base.py: -------------------------------------------------------------------------------- 1 | import os 2 | import datetime 3 | from pathlib import Path 4 | 5 | BASE_DIR = Path(__file__).resolve().parent.parent.parent 6 | TEMPLATES_DIR = BASE_DIR / 'templates' 7 | 8 | LANGUAGE_CODE = 'en-us' 9 | TIME_ZONE = 'UTC' 10 | USE_I18N = True 11 | USE_L10N = True 12 | USE_TZ = True 13 | 14 | INSTALLED_APPS = [ 15 | 'logui_apps.control', 16 | 'logui_apps.control_api', 17 | 'logui_apps.websocket', 18 | 'logui_apps.errorhandling', 19 | 'django.contrib.admin', 20 | 'django.contrib.auth', 21 | 'django.contrib.contenttypes', 22 | 'django.contrib.sessions', 23 | 'django.contrib.messages', 24 | 'django.contrib.staticfiles', 25 | 'channels', 26 | 'compressor', 27 | 'corsheaders', 28 | 'rest_framework', 29 | ] 30 | 31 | MIDDLEWARE = [ 32 | 'corsheaders.middleware.CorsMiddleware', 33 | 'django.middleware.security.SecurityMiddleware', 34 | 'django.contrib.sessions.middleware.SessionMiddleware', 35 | 'django.middleware.common.CommonMiddleware', 36 | 'django.middleware.csrf.CsrfViewMiddleware', 37 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 38 | 'django.contrib.messages.middleware.MessageMiddleware', 39 | 'django.middleware.clickjacking.XFrameOptionsMiddleware', 40 | ] 41 | 42 | ROOT_URLCONF = 'worker.urls' 43 | 44 | TEMPLATES = [ 45 | { 46 | 'BACKEND': 'django.template.backends.django.DjangoTemplates', 47 | 'DIRS': [TEMPLATES_DIR,], 48 | 'APP_DIRS': True, 49 | 'OPTIONS': { 50 | 'context_processors': [ 51 | 'django.template.context_processors.debug', 52 | 'django.template.context_processors.request', 53 | 'django.contrib.auth.context_processors.auth', 54 | 'django.contrib.messages.context_processors.messages', 55 | ], 56 | }, 57 | }, 58 | ] 59 | 60 | WSGI_APPLICATION = 'worker.wsgi.logui_application' 61 | ASGI_APPLICATION = 'worker.asgi.logui_application' 62 | 63 | AUTH_PASSWORD_VALIDATORS = [ 64 | { 65 | 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', 66 | }, 67 | { 68 | 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', 69 | }, 70 | { 71 | 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', 72 | }, 73 | { 74 | 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', 75 | }, 76 | ] 77 | 78 | STATIC_URL = '/static/' 79 | 80 | COMPRESS_ENABLED = True 81 | COMPRESS_OFFLINE = True 82 | COMPRESS_OUTPUT_DIR = 'cache' 83 | 84 | COMPRESS_PRECOMPILERS = ( 85 | ('text/less', 'lessc {infile} {outfile}'), 86 | ('text/jsx', 'cat {infile} | babel > {outfile}'), 87 | ('module', 'browserify {infile} -t babelify --outfile {outfile}'), 88 | ) 89 | 90 | COMPRESS_CSS_FILTERS = [ 91 | 'compressor.filters.css_default.CssAbsoluteFilter', 92 | 'compressor.filters.cssmin.rCSSMinFilter', 93 | ] 94 | 95 | COMPRESS_JS_FILTERS = [ 96 | 'compressor.filters.jsmin.JSMinFilter', 97 | 'compressor.filters.jsmin.CalmjsFilter', 98 | ] 99 | 100 | CORS_ALLOWED_ORIGINS = [ 101 | 'http://localhost:8000' # What do we need to change this to? 102 | ] 103 | 104 | REST_FRAMEWORK = { 105 | 'DEFAULT_PERMISSION_CLASSES': ( 106 | 'rest_framework.permissions.IsAuthenticated', 107 | ), 108 | 'DEFAULT_AUTHENTICATION_CLASSES': ( 109 | 'rest_framework_jwt.authentication.JSONWebTokenAuthentication', 110 | 'rest_framework.authentication.SessionAuthentication', 111 | 'rest_framework.authentication.BasicAuthentication', 112 | ), 113 | } 114 | 115 | JWT_AUTH = { 116 | 'JWT_RESPONSE_PAYLOAD_HANDLER': 'logui_apps.control_api.utils.token_response_handler', 117 | 'JWT_EXPIRATION_DELTA': datetime.timedelta(days=1), 118 | } -------------------------------------------------------------------------------- /worker/worker/settings/development.py: -------------------------------------------------------------------------------- 1 | from .base import * 2 | 3 | STATIC_DIR = BASE_DIR.parent / 'static' 4 | CLIENT_APP_DIR = BASE_DIR.parent / 'app' 5 | 6 | SECRET_KEY = 'developmentkey' 7 | DEBUG = True 8 | ALLOWED_HOSTS = ['localhost'] 9 | 10 | DATABASES = { 11 | 'default': { 12 | 'ENGINE': 'django.db.backends.sqlite3', 13 | 'NAME': BASE_DIR / 'db.sqlite3', 14 | } 15 | } 16 | 17 | STATICFILES_FINDERS = ( 18 | 'compressor.finders.CompressorFinder', 19 | 'django.contrib.staticfiles.finders.FileSystemFinder', 20 | 'django.contrib.staticfiles.finders.AppDirectoriesFinder', 21 | ) 22 | 23 | STATIC_ROOT = BASE_DIR / 'collectedstatic' 24 | COMPRESS_ROOT = STATIC_ROOT 25 | 26 | STATICFILES_DIRS = [ 27 | STATIC_DIR, 28 | CLIENT_APP_DIR, 29 | ] -------------------------------------------------------------------------------- /worker/worker/settings/docker.py: -------------------------------------------------------------------------------- 1 | from .base import * 2 | 3 | STATIC_DIR = BASE_DIR / 'static' 4 | COPIED_STATIC_DIR = BASE_DIR / 'copied-static' 5 | CLIENT_APP_DIR = BASE_DIR / 'app' 6 | 7 | SECRET_KEY = os.getenv('SECRET_KEY') 8 | DEBUG = False if os.getenv('DEBUG') == 'False' else True 9 | ALLOWED_HOSTS = ['127.0.0.1', 'localhost', os.getenv('DOCKER_HOSTNAME')] 10 | 11 | DATABASES = { 12 | 'default': { 13 | 'ENGINE': 'django.db.backends.postgresql_psycopg2', 14 | 'NAME': 'logui', 15 | 'USER': 'postgres', 16 | 'PASSWORD': os.getenv('DATABASE_PASSWORD'), 17 | 'HOST': 'db', 18 | 'PORT': '5432', 19 | } 20 | } 21 | 22 | STATICFILES_FINDERS = ( 23 | 'django.contrib.staticfiles.finders.FileSystemFinder', 24 | 'django.contrib.staticfiles.finders.AppDirectoriesFinder', 25 | 'compressor.finders.CompressorFinder', 26 | ) 27 | 28 | STATIC_ROOT = STATIC_DIR 29 | COMPRESS_ROOT = STATIC_DIR 30 | 31 | STATICFILES_DIRS = [ 32 | COPIED_STATIC_DIR, 33 | CLIENT_APP_DIR, 34 | ] 35 | 36 | MONGO_HOST = 'mongo' 37 | MONGO_PORT = 27017 38 | MONGO_DB = 'logui-db' 39 | MONGO_USERNAME = 'mongo' 40 | MONGO_PASSWORD = os.getenv('DATABASE_PASSWORD') -------------------------------------------------------------------------------- /worker/worker/urls.py: -------------------------------------------------------------------------------- 1 | from logui_apps.errorhandling import views as error_views 2 | from django.contrib import admin 3 | from django.urls import path, include 4 | 5 | urlpatterns = [ 6 | path('admin/', admin.site.urls), 7 | path('api/', include('logui_apps.control_api.urls')), 8 | path('', include('logui_apps.control.urls')), 9 | ] 10 | 11 | handler404 = error_views.handler_404_error 12 | handler500 = error_views.handler_500_error -------------------------------------------------------------------------------- /worker/worker/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for worker project. 3 | 4 | It exposes the WSGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/3.1/howto/deployment/wsgi/ 8 | """ 9 | 10 | import os 11 | from django.core.wsgi import get_wsgi_application 12 | from worker.settings import set_django_settings_module 13 | 14 | set_django_settings_module() 15 | 16 | logui_application = get_wsgi_application() 17 | --------------------------------------------------------------------------------